@bluemarble/bi 0.2.1 → 0.3.1

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,9 +1,10 @@
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";
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";
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-DN77UDVM.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, Tabs, TextField, ThemeProvider, ToggleButton, ToggleButtonGroup, Tooltip, Typography, createTheme } from "@mui/material";
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";
5
5
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { createPortal } from "react-dom";
7
8
  //#region src/devtools/sqlHighlight.tsx
8
9
  const KEYWORDS$1 = new Set(`SELECT FROM WHERE AND OR NOT IN IS NULL JOIN LEFT RIGHT INNER OUTER CROSS ON AS GROUP BY ORDER
9
10
  HAVING LIMIT OFFSET UNION ALL DISTINCT CASE WHEN THEN ELSE END WITH ASC DESC BETWEEN LIKE EXISTS
@@ -91,9 +92,13 @@ function StatusDot({ status }) {
91
92
  }
92
93
  });
93
94
  }
94
- async function copyText(text) {
95
+ /**
96
+ * Copia para a área de transferência. Com o devtools em janela separada, o clipboard tem que ser o
97
+ * dela (o da página não está em foco e recusa): passe o elemento clicado.
98
+ */
99
+ async function copyText(text, from) {
95
100
  try {
96
- await navigator.clipboard.writeText(text);
101
+ await (from?.ownerDocument.defaultView ?? window).navigator.clipboard.writeText(text);
97
102
  return true;
98
103
  } catch {
99
104
  return false;
@@ -103,8 +108,8 @@ function CopyButton({ text, label = "Copiar" }) {
103
108
  const [copied, setCopied] = useState(false);
104
109
  return /* @__PURE__ */ jsx(Button, {
105
110
  size: "small",
106
- onClick: async () => {
107
- if (await copyText(text)) {
111
+ onClick: async (event) => {
112
+ if (await copyText(text, event.currentTarget)) {
108
113
  setCopied(true);
109
114
  setTimeout(() => setCopied(false), 1200);
110
115
  }
@@ -112,9 +117,33 @@ function CopyButton({ text, label = "Copiar" }) {
112
117
  children: copied ? "Copiado!" : label
113
118
  });
114
119
  }
115
- function Section({ title, actions, children }) {
120
+ const COLLAPSED_KEY = "bm-dashboard-devtools:collapsed";
121
+ function loadCollapsed() {
122
+ try {
123
+ const saved = JSON.parse(localStorage.getItem(COLLAPSED_KEY) ?? "[]");
124
+ return Array.isArray(saved) ? saved : [];
125
+ } catch {
126
+ return [];
127
+ }
128
+ }
129
+ /** Seção recolhida (lembrada entre recargas, pela chave). */
130
+ function useCollapsed(key) {
131
+ const [collapsed, setCollapsed] = useState(() => !!key && loadCollapsed().includes(key));
132
+ const toggle = () => {
133
+ if (!key) return;
134
+ const next = !collapsed;
135
+ setCollapsed(next);
136
+ try {
137
+ const others = loadCollapsed().filter((item) => item !== key);
138
+ localStorage.setItem(COLLAPSED_KEY, JSON.stringify(next ? [...others, key] : others));
139
+ } catch {}
140
+ };
141
+ return [collapsed, toggle];
142
+ }
143
+ function Section({ title, actions, collapseKey, children }) {
144
+ const [collapsed, toggle] = useCollapsed(collapseKey);
116
145
  return /* @__PURE__ */ jsxs(Box, {
117
- sx: { mb: 2 },
146
+ sx: { mb: collapsed ? .5 : 2 },
118
147
  children: [/* @__PURE__ */ jsxs(Box, {
119
148
  sx: {
120
149
  display: "flex",
@@ -123,15 +152,43 @@ function Section({ title, actions, children }) {
123
152
  mb: .5,
124
153
  minHeight: 30
125
154
  },
126
- children: [/* @__PURE__ */ jsx(Typography, {
155
+ children: [/* @__PURE__ */ jsxs(Typography, {
127
156
  variant: "overline",
157
+ component: collapseKey ? "button" : "span",
158
+ ...collapseKey && {
159
+ type: "button",
160
+ onClick: toggle,
161
+ "aria-expanded": !collapsed,
162
+ title: collapsed ? "Expandir" : "Recolher"
163
+ },
128
164
  sx: {
129
165
  color: "text.secondary",
130
166
  lineHeight: 1.5,
131
- fontWeight: 700
167
+ fontWeight: 700,
168
+ ...collapseKey && {
169
+ display: "flex",
170
+ alignItems: "center",
171
+ gap: .5,
172
+ p: 0,
173
+ border: 0,
174
+ bgcolor: "transparent",
175
+ cursor: "pointer",
176
+ textAlign: "left",
177
+ "&:hover": { color: "text.primary" }
178
+ }
132
179
  },
133
- children: title
134
- }), /* @__PURE__ */ jsx(Box, {
180
+ children: [collapseKey && /* @__PURE__ */ jsx(Box, {
181
+ component: "span",
182
+ "aria-hidden": true,
183
+ sx: {
184
+ display: "inline-block",
185
+ fontSize: 9,
186
+ transition: "transform .15s",
187
+ transform: collapsed ? "rotate(-90deg)" : "none"
188
+ },
189
+ children: "▼"
190
+ }), title]
191
+ }), !collapsed && /* @__PURE__ */ jsx(Box, {
135
192
  sx: {
136
193
  ml: "auto",
137
194
  display: "flex",
@@ -139,7 +196,7 @@ function Section({ title, actions, children }) {
139
196
  },
140
197
  children: actions
141
198
  })]
142
- }), children]
199
+ }), !collapsed && children]
143
200
  });
144
201
  }
145
202
  function CodeBlock({ code, maxHeight = 260, sql = false }) {
@@ -181,7 +238,16 @@ function inlineParams(sql, params) {
181
238
  });
182
239
  }
183
240
  const MAX_ROWS = 500;
241
+ /** Ordena linhas de resultado por uma coluna; `null` sempre no fim. */
242
+ function compareCells(a, b, { column, direction }) {
243
+ const [x, y] = [a[column], b[column]];
244
+ if (x === null || x === void 0) return y === null || y === void 0 ? 0 : 1;
245
+ if (y === null || y === void 0) return -1;
246
+ const order = typeof x === "number" && typeof y === "number" ? x - y : String(x).localeCompare(String(y), "pt-BR", { numeric: true });
247
+ return direction === "asc" ? order : -order;
248
+ }
184
249
  function ResultTable({ rows, maxHeight = 320 }) {
250
+ const [sort, setSort] = useState(null);
185
251
  if (!rows.length) return /* @__PURE__ */ jsx(Typography, {
186
252
  sx: {
187
253
  color: "text.secondary",
@@ -190,6 +256,14 @@ function ResultTable({ rows, maxHeight = 320 }) {
190
256
  children: "Nenhuma linha."
191
257
  });
192
258
  const columns = Object.keys(rows[0]);
259
+ const sorted = sort ? [...rows].sort((a, b) => compareCells(a, b, sort)) : rows;
260
+ const toggleSort = (column) => setSort((current) => current?.column !== column ? {
261
+ column,
262
+ direction: "asc"
263
+ } : current.direction === "asc" ? {
264
+ column,
265
+ direction: "desc"
266
+ } : null);
193
267
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TableContainer, {
194
268
  sx: {
195
269
  maxHeight,
@@ -201,12 +275,19 @@ function ResultTable({ rows, maxHeight = 320 }) {
201
275
  size: "small",
202
276
  stickyHeader: true,
203
277
  children: [/* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(TableCell, {
278
+ sortDirection: sort?.column === column ? sort.direction : false,
204
279
  sx: {
205
280
  fontFamily: MONO,
206
- fontWeight: 700
281
+ fontWeight: 700,
282
+ whiteSpace: "nowrap"
207
283
  },
208
- children: column
209
- }, column)) }) }), /* @__PURE__ */ jsx(TableBody, { children: rows.slice(0, MAX_ROWS).map((row, index) => /* @__PURE__ */ jsx(TableRow, {
284
+ children: /* @__PURE__ */ jsx(TableSortLabel, {
285
+ active: sort?.column === column,
286
+ direction: sort?.column === column ? sort.direction : "asc",
287
+ onClick: () => toggleSort(column),
288
+ children: column
289
+ })
290
+ }, column)) }) }), /* @__PURE__ */ jsx(TableBody, { children: sorted.slice(0, MAX_ROWS).map((row, index) => /* @__PURE__ */ jsx(TableRow, {
210
291
  hover: true,
211
292
  children: columns.map((column) => /* @__PURE__ */ jsx(TableCell, {
212
293
  align: typeof row[column] === "number" ? "right" : "left",
@@ -508,6 +589,126 @@ function FiltersPanel({ devtools }) {
508
589
  });
509
590
  }
510
591
  //#endregion
592
+ //#region src/devtools/DevtoolsWindow.tsx
593
+ /**
594
+ * Devtools numa janela separada do navegador (popup), para usar com a página em tela cheia ou em
595
+ * outro monitor. O painel continua sendo renderizado pela árvore React da página (mesmo contexto,
596
+ * mesmo store), só que via portal no `document` do popup:
597
+ *
598
+ * - os estilos do emotion/MUI vão para o `<head>` da página; aqui eles são espelhados no popup;
599
+ * - menus, tooltips e diálogos do MUI montam no `body` da página por padrão: o tema da janela
600
+ * aponta o `container` deles para o popup.
601
+ *
602
+ * Recarregar a página derruba o vínculo (o popup é de outra árvore): o popup avisa e o botão do
603
+ * devtools na página reconecta (abrir popup exige um clique, o navegador bloqueia sem gesto).
604
+ */
605
+ const WINDOW_NAME = "bm-bi-devtools";
606
+ /** Abre (ou reaproveita, se ainda estiver aberta) a janela do devtools. `null` = bloqueada. */
607
+ function openDevtoolsWindow() {
608
+ const width = Math.min(1100, window.screen.availWidth);
609
+ const height = Math.min(720, window.screen.availHeight);
610
+ return window.open("", WINDOW_NAME, `popup,width=${width},height=${height}`);
611
+ }
612
+ const isStyleNode = (node) => node instanceof HTMLStyleElement || node instanceof HTMLLinkElement && node.rel === "stylesheet";
613
+ /** Cópia de um `<style>`/`<link>`; estilo inserido por `insertRule` (sem texto) vai pelas regras. */
614
+ function cloneStyle(node, target) {
615
+ const clone = target.importNode(node, true);
616
+ if (node instanceof HTMLStyleElement && !node.textContent && node.sheet?.cssRules.length) clone.textContent = [...node.sheet.cssRules].map((rule) => rule.cssText).join("\n");
617
+ return clone;
618
+ }
619
+ /** Copia os estilos da página para o popup e acompanha os que entrarem/saírem depois. */
620
+ function mirrorStyles(source, target) {
621
+ const clones = /* @__PURE__ */ new Map();
622
+ const add = (node) => {
623
+ if (!isStyleNode(node) || clones.has(node)) return;
624
+ const clone = cloneStyle(node, target);
625
+ clones.set(node, clone);
626
+ target.head.appendChild(clone);
627
+ };
628
+ for (const node of source.head.querySelectorAll("style, link[rel=\"stylesheet\"]")) add(node);
629
+ const observer = new MutationObserver((mutations) => {
630
+ for (const mutation of mutations) {
631
+ if (mutation.type === "characterData" || mutation.target !== source.head) {
632
+ const style = (mutation.target instanceof Element ? mutation.target : mutation.target.parentElement)?.closest("style");
633
+ const clone = style && clones.get(style);
634
+ if (style && clone) {
635
+ const next = cloneStyle(style, target);
636
+ target.head.replaceChild(next, clone);
637
+ clones.set(style, next);
638
+ }
639
+ continue;
640
+ }
641
+ for (const node of mutation.addedNodes) add(node);
642
+ for (const node of mutation.removedNodes) {
643
+ clones.get(node)?.parentNode?.removeChild(clones.get(node));
644
+ clones.delete(node);
645
+ }
646
+ }
647
+ });
648
+ observer.observe(source.head, {
649
+ childList: true,
650
+ subtree: true,
651
+ characterData: true
652
+ });
653
+ return () => observer.disconnect();
654
+ }
655
+ /** Tema com os portais do MUI (menus, tooltips, diálogos) dentro do popup. */
656
+ function windowTheme(base, container) {
657
+ return createTheme(base, { components: {
658
+ MuiModal: { defaultProps: { container } },
659
+ MuiPopover: { defaultProps: { container } },
660
+ MuiPopper: { defaultProps: { container } },
661
+ MuiDialog: { defaultProps: { container } },
662
+ MuiMenu: { defaultProps: { container } }
663
+ } });
664
+ }
665
+ function DevtoolsWindow({ popup, title, theme, onClosed, children }) {
666
+ const [root, setRoot] = useState(null);
667
+ const onClosedRef = useRef(onClosed);
668
+ onClosedRef.current = onClosed;
669
+ useEffect(() => {
670
+ const doc = popup.document;
671
+ doc.title = title;
672
+ doc.head.replaceChildren();
673
+ doc.body.replaceChildren();
674
+ const meta = doc.createElement("meta");
675
+ meta.setAttribute("charset", "utf-8");
676
+ doc.head.appendChild(meta);
677
+ const base = doc.createElement("style");
678
+ base.textContent = `html, body { height: 100%; margin: 0; background: ${theme.palette.background.paper}; color-scheme: dark; } body { font-family: ${theme.typography.fontFamily}; } #${WINDOW_NAME} { height: 100%; display: flex; flex-direction: column; }`;
679
+ doc.head.appendChild(base);
680
+ const container = doc.createElement("div");
681
+ container.id = WINDOW_NAME;
682
+ doc.body.appendChild(container);
683
+ const stopMirror = mirrorStyles(document, doc);
684
+ setRoot(container);
685
+ const poll = setInterval(() => {
686
+ if (popup.closed) onClosedRef.current();
687
+ }, 400);
688
+ const onPageHide = () => {
689
+ stopMirror();
690
+ doc.body.innerHTML = `<p style="font: 14px sans-serif; color: #ccc; padding: 24px">A página do dashboard recarregou. Clique no botão <b style="color: #F58220">◆ BI</b> da página para reconectar o devtools a esta janela.</p>`;
691
+ };
692
+ window.addEventListener("pagehide", onPageHide);
693
+ return () => {
694
+ clearInterval(poll);
695
+ window.removeEventListener("pagehide", onPageHide);
696
+ stopMirror();
697
+ setRoot(null);
698
+ };
699
+ }, [
700
+ popup,
701
+ title,
702
+ theme
703
+ ]);
704
+ const localTheme = useMemo(() => root && windowTheme(theme, root), [theme, root]);
705
+ if (!root || !localTheme) return null;
706
+ return createPortal(/* @__PURE__ */ jsx(ThemeProvider, {
707
+ theme: localTheme,
708
+ children
709
+ }), root);
710
+ }
711
+ //#endregion
511
712
  //#region src/devtools/Inspector.tsx
512
713
  /**
513
714
  * Liga componente da página <-> consulta do devtools. Os componentes marcam o elemento raiz com
@@ -676,7 +877,7 @@ function useWidth() {
676
877
  const [width, setWidth] = useState(Number.POSITIVE_INFINITY);
677
878
  useEffect(() => {
678
879
  if (!element) return;
679
- const observer = new ResizeObserver(([entry]) => entry && setWidth(entry.contentRect.width));
880
+ const observer = new ((element.ownerDocument.defaultView?.ResizeObserver) ?? ResizeObserver)(([entry]) => entry && setWidth(entry.contentRect.width));
680
881
  observer.observe(element);
681
882
  return () => observer.disconnect();
682
883
  }, [element]);
@@ -696,6 +897,18 @@ const uniqueId = (charts, base) => {
696
897
  while (ids.has(`${base}-${n}`)) n++;
697
898
  return `${base}-${n}`;
698
899
  };
900
+ /** Objeto sem nenhum campo sai do JSON. */
901
+ const nonEmpty = (object) => Object.keys(object).length ? object : void 0;
902
+ /** Campos do card de um gráfico, com o padrão do layout (`cards`) no lugar do padrão da lib. */
903
+ const chartCardFields = (cards) => Object.fromEntries(Object.entries(CARD_FIELDS).map(([key, spec]) => [key, {
904
+ ...spec,
905
+ default: cards?.[key] ?? spec.default
906
+ }]));
907
+ const cardsSummary = (cards) => {
908
+ const { gap, ...style } = cards ?? {};
909
+ const custom = Object.keys(style).length;
910
+ return `espaço ${String(gap ?? CARDS_FIELDS.gap?.default)}px · ${custom ? `${custom} ajuste(s) de estilo` : "estilo padrão"}`;
911
+ };
699
912
  /** Campo opcional vazio sai do objeto, para o JSON ficar só com o que foi definido. */
700
913
  function withField(object, key, value) {
701
914
  const next = { ...object };
@@ -809,7 +1022,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
809
1022
  const charts = editing.charts;
810
1023
  const filters = editing.filters ?? [];
811
1024
  const selectedFilter = selection?.kind === "filter" ? filters.find((filter) => filter.dimension === selection.key) ?? null : null;
812
- const selected = selection?.kind === "filter" || selection?.kind === "canvas" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
1025
+ const selected = selection?.kind === "filter" || selection?.kind === "canvas" || selection?.kind === "cards" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
813
1026
  const layoutKey = entry.registered.id;
814
1027
  const differsFromFile = canonicalJson(editing) !== canonicalJson(entry.registered);
815
1028
  const update = (next) => {
@@ -826,6 +1039,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
826
1039
  });
827
1040
  const patchChart = (id, change) => setCharts(charts.map((chart) => chart.id === id ? change(chart) : chart));
828
1041
  const setCanvas = (next) => update(withField(editing, "canvas", next));
1042
+ const setCards = (key, value) => update(withField(editing, "cards", nonEmpty(withField(editing.cards ?? {}, key, value))));
829
1043
  const setFilters = (next) => update({
830
1044
  ...editing,
831
1045
  filters: next
@@ -884,7 +1098,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
884
1098
  };
885
1099
  const stacked = rootWidth < STACK_WIDTH;
886
1100
  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 : "";
1101
+ const selectedTitle = selection?.kind === "canvas" ? "Tela" : selection?.kind === "cards" ? "Cards" : selectedFilter ? selectedFilter.label || model.dimensions[selectedFilter.dimension]?.label || selectedFilter.dimension : selected ? selected.title || selected.id : "";
888
1102
  const hover = (item) => onHover(item && itemQueryId(layoutKey, item));
889
1103
  const add = (type, base) => {
890
1104
  setAddMenu(null);
@@ -992,9 +1206,10 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
992
1206
  children: [item.registered.id, item.draft ? " · rascunho" : ""]
993
1207
  }, item.registered.id))
994
1208
  }),
995
- /* @__PURE__ */ jsx(Section, {
1209
+ /* @__PURE__ */ jsxs(Section, {
996
1210
  title: "Tela",
997
- children: /* @__PURE__ */ jsx(ItemRow, {
1211
+ collapseKey: "layout:lista:tela",
1212
+ children: [/* @__PURE__ */ jsx(ItemRow, {
998
1213
  title: editing.canvas ? "Tamanho fixo" : "Responsiva",
999
1214
  subtitle: editing.canvas ? presetLabel(editing.canvas) : "reorganiza conforme a largura",
1000
1215
  selected: selection?.kind === "canvas",
@@ -1002,10 +1217,19 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1002
1217
  setSelection({ kind: "canvas" });
1003
1218
  if (stacked) setSidebar(false);
1004
1219
  }
1005
- })
1220
+ }), /* @__PURE__ */ jsx(ItemRow, {
1221
+ title: "Cards",
1222
+ subtitle: cardsSummary(editing.cards),
1223
+ selected: selection?.kind === "cards",
1224
+ onSelect: () => {
1225
+ setSelection({ kind: "cards" });
1226
+ if (stacked) setSidebar(false);
1227
+ }
1228
+ })]
1006
1229
  }),
1007
1230
  /* @__PURE__ */ jsxs(Section, {
1008
1231
  title: `Filtros (${filters.length})`,
1232
+ collapseKey: "layout:lista:filtros",
1009
1233
  actions: /* @__PURE__ */ jsx(Button, {
1010
1234
  size: "small",
1011
1235
  onClick: addFilter,
@@ -1037,6 +1261,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1037
1261
  }),
1038
1262
  /* @__PURE__ */ jsx(Section, {
1039
1263
  title: `Gráficos (${charts.length})`,
1264
+ collapseKey: "layout:lista:graficos",
1040
1265
  actions: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
1041
1266
  size: "small",
1042
1267
  onClick: (e) => setAddMenu(e.currentTarget),
@@ -1113,7 +1338,23 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1113
1338
  canvas: editing.canvas,
1114
1339
  columns: formColumns,
1115
1340
  onChange: setCanvas
1116
- }) : selectedFilter ? /* @__PURE__ */ jsx(FilterForm, {
1341
+ }) : selection?.kind === "cards" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Typography, {
1342
+ sx: {
1343
+ fontSize: 12,
1344
+ color: "text.secondary",
1345
+ mb: 1
1346
+ },
1347
+ children: "Estilo padrão dos cards de todos os gráficos; cada gráfico pode sobrepor na seção Card dele. Vazio = padrão do componente."
1348
+ }), /* @__PURE__ */ jsx(FieldsForm, {
1349
+ fields: CARDS_FIELDS,
1350
+ values: editing.cards ?? {},
1351
+ onSet: setCards,
1352
+ model,
1353
+ formatters: [],
1354
+ columns: [],
1355
+ formColumns,
1356
+ chosenDimensions: []
1357
+ })] }) : selectedFilter ? /* @__PURE__ */ jsx(FilterForm, {
1117
1358
  filter: selectedFilter,
1118
1359
  model,
1119
1360
  formatters: entry.parts.filters ?? [],
@@ -1127,6 +1368,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1127
1368
  }
1128
1369
  }, `filter:${selectedFilter.dimension}`) : selected ? /* @__PURE__ */ jsx(ChartForm, {
1129
1370
  chart: selected,
1371
+ cards: editing.cards,
1130
1372
  types: chartTypes,
1131
1373
  model,
1132
1374
  formatters: entry.parts.charts ?? [],
@@ -1234,6 +1476,7 @@ function CanvasForm({ canvas, columns, onChange }) {
1234
1476
  const number = (value) => value === "" ? void 0 : Number(value);
1235
1477
  return /* @__PURE__ */ jsx(Section, {
1236
1478
  title: "Tela",
1479
+ collapseKey: "layout:form:Tela",
1237
1480
  children: /* @__PURE__ */ jsxs(Box, {
1238
1481
  sx: formGrid(columns),
1239
1482
  children: [/* @__PURE__ */ jsx(Box, {
@@ -1401,9 +1644,10 @@ function SmallButton({ title, disabled, onClick, children }) {
1401
1644
  }) })
1402
1645
  });
1403
1646
  }
1404
- function ChartForm({ chart, types, model, formatters, columns, onChange }) {
1647
+ function ChartForm({ chart, cards, types, model, formatters, columns, onChange }) {
1405
1648
  const set = (key, value) => onChange((current) => withField(current, key, value));
1406
- return /* @__PURE__ */ jsx(FieldsForm, {
1649
+ const setCard = (key, value) => onChange((current) => withField(current, "card", nonEmpty(withField(current.card ?? {}, key, value))));
1650
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FieldsForm, {
1407
1651
  fields: types[chart.type]?.fields ?? {},
1408
1652
  values: chart,
1409
1653
  onSet: set,
@@ -1435,7 +1679,16 @@ function ChartForm({ chart, types, model, formatters, columns, onChange }) {
1435
1679
  onChange: (e) => set("id", e.target.value),
1436
1680
  sx: inputSx$1
1437
1681
  }), "Identificador no JSON (letras, números, \"-\" e \"_\")")] })
1438
- });
1682
+ }), /* @__PURE__ */ jsx(FieldsForm, {
1683
+ fields: chartCardFields(cards),
1684
+ values: chart.card ?? {},
1685
+ onSet: setCard,
1686
+ model,
1687
+ formatters: [],
1688
+ columns: [],
1689
+ formColumns: columns,
1690
+ chosenDimensions: []
1691
+ })] });
1439
1692
  }
1440
1693
  function FilterForm({ filter, model, formatters, columns, onChange }) {
1441
1694
  return /* @__PURE__ */ jsx(FieldsForm, {
@@ -1625,6 +1878,7 @@ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, fo
1625
1878
  const inSection = entries.filter(([, spec]) => spec.section === section);
1626
1879
  return /* @__PURE__ */ jsx(Section, {
1627
1880
  title: section,
1881
+ collapseKey: `layout:form:${section}`,
1628
1882
  children: /* @__PURE__ */ jsxs(Box, {
1629
1883
  sx: formGrid(formColumns),
1630
1884
  children: [section === sections[0] && header, inSection.map(([key, spec]) => /* @__PURE__ */ jsx(Box, {
@@ -2705,6 +2959,7 @@ function track(event, onMove, onUp) {
2705
2959
  x: event.clientX,
2706
2960
  y: event.clientY
2707
2961
  };
2962
+ const win = event.currentTarget.ownerDocument.defaultView ?? window;
2708
2963
  let moved = false;
2709
2964
  const move = (moveEvent) => {
2710
2965
  const dx = moveEvent.clientX - start.x;
@@ -2713,12 +2968,12 @@ function track(event, onMove, onUp) {
2713
2968
  if (moved) onMove(dx, dy, moveEvent);
2714
2969
  };
2715
2970
  const up = (upEvent) => {
2716
- window.removeEventListener("mousemove", move);
2717
- window.removeEventListener("mouseup", up);
2971
+ win.removeEventListener("mousemove", move);
2972
+ win.removeEventListener("mouseup", up);
2718
2973
  onUp?.(upEvent, moved);
2719
2974
  };
2720
- window.addEventListener("mousemove", move);
2721
- window.addEventListener("mouseup", up);
2975
+ win.addEventListener("mousemove", move);
2976
+ win.addEventListener("mouseup", up);
2722
2977
  }
2723
2978
  /** Colunas citadas como `tabela.coluna` no SQL das dimensões/medidas. */
2724
2979
  function citedColumns(sql, table) {
@@ -3060,7 +3315,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3060
3315
  selectNode(node);
3061
3316
  return;
3062
3317
  }
3063
- const target = document.elementFromPoint(upEvent.clientX, upEvent.clientY)?.closest("[data-diagram-column]");
3318
+ const target = (canvas.current?.ownerDocument ?? document).elementFromPoint(upEvent.clientX, upEvent.clientY)?.closest("[data-diagram-column]");
3064
3319
  const targetNode = nodes.find((item) => item.id === target?.dataset.diagramNode);
3065
3320
  const targetColumn = target?.dataset.diagramColumn;
3066
3321
  if (targetNode && targetColumn) connect(node, column, targetNode, targetColumn);
@@ -4587,7 +4842,7 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
4587
4842
  }),
4588
4843
  /* @__PURE__ */ jsx(Button, {
4589
4844
  size: "small",
4590
- onClick: () => copyText(toModelCode(edited)),
4845
+ onClick: (e) => copyText(toModelCode(edited), e.currentTarget),
4591
4846
  children: "Copiar TS"
4592
4847
  }),
4593
4848
  /* @__PURE__ */ jsx(Button, {
@@ -4850,12 +5105,55 @@ function PlaygroundPanel({ model, selection, onSelectionChange }) {
4850
5105
  //#endregion
4851
5106
  //#region src/devtools/QueriesPanel.tsx
4852
5107
  const queryKey = (run) => JSON.stringify(run.query);
5108
+ const SORT_LABEL = {
5109
+ default: "ordem da página",
5110
+ label: "nome",
5111
+ duration: "mais lentas",
5112
+ rows: "mais linhas",
5113
+ status: "erros primeiro"
5114
+ };
5115
+ const SORT_KEY = "bm-dashboard-devtools:queries-sort";
5116
+ /** Problemas primeiro: erro, em andamento, cancelada, ok. */
5117
+ const STATUS_ORDER = {
5118
+ error: 0,
5119
+ loading: 1,
5120
+ cancelled: 2,
5121
+ success: 3
5122
+ };
5123
+ function loadSort() {
5124
+ try {
5125
+ const saved = localStorage.getItem(SORT_KEY);
5126
+ return saved && saved in SORT_LABEL ? saved : "default";
5127
+ } catch {
5128
+ return "default";
5129
+ }
5130
+ }
5131
+ /** Ordena a lista; empate mantém a ordem original (página ou mais recente primeiro). */
5132
+ function sortRuns(runs, key) {
5133
+ if (key === "default") return runs;
5134
+ const compare = (a, b) => {
5135
+ switch (key) {
5136
+ case "label": return a.label.localeCompare(b.label, "pt-BR", { numeric: true });
5137
+ case "duration": return (b.durationMs ?? Date.now() - b.startedAt) - (a.durationMs ?? Date.now() - a.startedAt);
5138
+ case "rows": return (b.rows?.length ?? -1) - (a.rows?.length ?? -1);
5139
+ case "status": return STATUS_ORDER[a.status] - STATUS_ORDER[b.status];
5140
+ }
5141
+ };
5142
+ return [...runs].sort(compare);
5143
+ }
4853
5144
  /** Consultas ativas na página (uma por componente) e o histórico de execuções. */
4854
5145
  function QueriesPanel({ devtools, capabilities, selected, onSelect, onHover, onClearServerCache }) {
4855
5146
  const { store, refresh } = devtools;
4856
5147
  const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
4857
5148
  const [view, setView] = useState("active");
4858
- const runs = view === "active" ? snapshot.instances.map((instance) => instance.last) : snapshot.history;
5149
+ const [sort, setSort] = useState(loadSort);
5150
+ const changeSort = (next) => {
5151
+ setSort(next);
5152
+ try {
5153
+ localStorage.setItem(SORT_KEY, next);
5154
+ } catch {}
5155
+ };
5156
+ const runs = sortRuns(view === "active" ? snapshot.instances.map((instance) => instance.last) : snapshot.history, sort);
4859
5157
  const instanceById = new Map(snapshot.instances.map((item) => [item.instanceId, item]));
4860
5158
  const sameQuery = /* @__PURE__ */ new Map();
4861
5159
  for (const { last } of snapshot.instances) sameQuery.set(queryKey(last), (sameQuery.get(queryKey(last)) ?? 0) + 1);
@@ -4886,31 +5184,54 @@ function QueriesPanel({ devtools, capabilities, selected, onSelect, onHover, onC
4886
5184
  borderBottom: 1,
4887
5185
  borderColor: "divider"
4888
5186
  },
4889
- children: [/* @__PURE__ */ jsxs(ToggleButtonGroup, {
4890
- size: "small",
4891
- exclusive: true,
4892
- value: view,
4893
- onChange: (_, value) => value && setView(value),
4894
- children: [/* @__PURE__ */ jsxs(ToggleButton, {
4895
- value: "active",
4896
- children: [
4897
- "Ativas (",
4898
- snapshot.instances.length,
4899
- ")"
4900
- ]
4901
- }), /* @__PURE__ */ jsxs(ToggleButton, {
4902
- value: "history",
4903
- children: [
4904
- "Histórico (",
4905
- snapshot.history.length,
4906
- ")"
4907
- ]
4908
- })]
4909
- }), view === "history" && /* @__PURE__ */ jsx(Button, {
4910
- size: "small",
4911
- onClick: store.clearHistory,
4912
- children: "Limpar"
4913
- })]
5187
+ children: [
5188
+ /* @__PURE__ */ jsxs(ToggleButtonGroup, {
5189
+ size: "small",
5190
+ exclusive: true,
5191
+ value: view,
5192
+ onChange: (_, value) => value && setView(value),
5193
+ children: [/* @__PURE__ */ jsxs(ToggleButton, {
5194
+ value: "active",
5195
+ children: [
5196
+ "Ativas (",
5197
+ snapshot.instances.length,
5198
+ ")"
5199
+ ]
5200
+ }), /* @__PURE__ */ jsxs(ToggleButton, {
5201
+ value: "history",
5202
+ children: [
5203
+ "Histórico (",
5204
+ snapshot.history.length,
5205
+ ")"
5206
+ ]
5207
+ })]
5208
+ }),
5209
+ view === "history" && /* @__PURE__ */ jsx(Button, {
5210
+ size: "small",
5211
+ onClick: store.clearHistory,
5212
+ children: "Limpar"
5213
+ }),
5214
+ /* @__PURE__ */ jsx(TextField, {
5215
+ select: true,
5216
+ size: "small",
5217
+ value: sort,
5218
+ onChange: (e) => changeSort(e.target.value),
5219
+ title: "Ordenar a lista",
5220
+ sx: {
5221
+ ml: "auto",
5222
+ minWidth: 0,
5223
+ "& .MuiSelect-select": {
5224
+ fontSize: 12,
5225
+ py: .5
5226
+ }
5227
+ },
5228
+ children: Object.keys(SORT_LABEL).map((key) => /* @__PURE__ */ jsx(MenuItem, {
5229
+ value: key,
5230
+ dense: true,
5231
+ children: key === "default" && view === "history" ? "mais recentes" : SORT_LABEL[key]
5232
+ }, key))
5233
+ })
5234
+ ]
4914
5235
  }),
4915
5236
  /* @__PURE__ */ jsxs(Box, {
4916
5237
  sx: {
@@ -5102,6 +5423,7 @@ function RunDetail({ run, devtools, capabilities, mounted }) {
5102
5423
  },
5103
5424
  children: run.error
5104
5425
  }),
5426
+ run.status === "error" && !debug && /* @__PURE__ */ jsx(FailedSql, { run }),
5105
5427
  /* @__PURE__ */ jsx(Section, {
5106
5428
  title: "Consulta",
5107
5429
  children: /* @__PURE__ */ jsx(CodeBlock, {
@@ -5149,6 +5471,67 @@ function RunDetail({ run, devtools, capabilities, mounted }) {
5149
5471
  })
5150
5472
  ] });
5151
5473
  }
5474
+ /**
5475
+ * SQL de uma consulta que deu erro: o servidor não devolve debug junto com o erro, então pede só a
5476
+ * compilação (`devtools.compile`), sem executar. Se nem compilar, mostra por quê.
5477
+ */
5478
+ function FailedSql({ run }) {
5479
+ const { fetcher } = useDashboard();
5480
+ const [state, setState] = useState(null);
5481
+ useEffect(() => {
5482
+ let active = true;
5483
+ setState({ loading: true });
5484
+ fetcher({
5485
+ ...run.query,
5486
+ devtools: { compile: true }
5487
+ }).then((result) => {
5488
+ const debug = result.devtools?.debug;
5489
+ if (active) setState(debug ? {
5490
+ loading: false,
5491
+ data: debug
5492
+ } : {
5493
+ loading: false,
5494
+ error: "O endpoint não devolveu o SQL (precisa de runQuery(..., { devtools: true }) numa versão que aceite devtools.compile)."
5495
+ });
5496
+ }).catch((error) => active && setState({
5497
+ loading: false,
5498
+ error: errorMessage(error)
5499
+ }));
5500
+ return () => {
5501
+ active = false;
5502
+ };
5503
+ }, [fetcher, run.query]);
5504
+ return /* @__PURE__ */ jsx(Section, {
5505
+ title: "SQL gerado (não executou)",
5506
+ children: /* @__PURE__ */ jsx(ToolResult, {
5507
+ state,
5508
+ children: (debug) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(CodeBlock, {
5509
+ sql: true,
5510
+ code: inlineParams(debug.sql, debug.params)
5511
+ }), /* @__PURE__ */ jsxs(Typography, {
5512
+ sx: {
5513
+ fontSize: 11,
5514
+ color: "text.secondary",
5515
+ mt: .5
5516
+ },
5517
+ children: [
5518
+ "Compilado de novo agora",
5519
+ debug.draft ? ", com o rascunho atual" : "",
5520
+ ", para depurar o erro acima.",
5521
+ !!debug.params.length && /* @__PURE__ */ jsxs(Box, {
5522
+ component: "span",
5523
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
5524
+ children: [
5525
+ " ",
5526
+ "params: ",
5527
+ JSON.stringify(debug.params)
5528
+ ]
5529
+ })
5530
+ ]
5531
+ })] })
5532
+ })
5533
+ });
5534
+ }
5152
5535
  function useTool() {
5153
5536
  const [state, setState] = useState(null);
5154
5537
  const run = async (load) => {
@@ -5632,9 +6015,10 @@ function CheckList({ checks, expectedLabel, actualLabel }) {
5632
6015
  * painel com as consultas da página, o modelo editável, um playground, regressão, dicionário e
5633
6016
  * filtros. Montado pelo próprio `DashboardProvider` em desenvolvimento.
5634
6017
  *
5635
- * Dois modos: fixado na parte de baixo da tela ou flutuante (janela que se arrasta pelo cabeçalho e
5636
- * redimensiona pelo canto). Fixado (embaixo, à esquerda ou à direita), o painel ocupa espaço: a
5637
- * página encolhe para não ficar escondida atrás dele; só o modo flutuante sobrepõe a página.
6018
+ * Modos: fixado (embaixo, à esquerda ou à direita), flutuante (janela que se arrasta pelo cabeçalho
6019
+ * e redimensiona pelo canto) ou numa janela separada do navegador (`DevtoolsWindow`). Fixado, o
6020
+ * painel ocupa espaço: a página encolhe para não ficar escondida atrás dele; só o modo flutuante
6021
+ * sobrepõe a página.
5638
6022
  * Atalhos: Alt+Shift+B abre/fecha o painel, Alt+Shift+F alterna entre fixado e flutuante,
5639
6023
  * Alt+Shift+I liga o inspetor, Esc sai do inspetor.
5640
6024
  */
@@ -5723,13 +6107,15 @@ function loadUi() {
5723
6107
  "bottom",
5724
6108
  "left",
5725
6109
  "right",
5726
- "floating"
6110
+ "floating",
6111
+ "window"
5727
6112
  ].includes(saved.mode)) saved.mode = "bottom";
5728
6113
  if (![
5729
6114
  "bottom",
5730
6115
  "left",
5731
6116
  "right"
5732
6117
  ].includes(saved.dock)) saved.dock = "bottom";
6118
+ if (saved.mode === "window") saved.open = false;
5733
6119
  return {
5734
6120
  ...saved,
5735
6121
  float: clampFloat({
@@ -5828,6 +6214,7 @@ function DashboardDevtools() {
5828
6214
  const [pickedItem, setPickedItem] = useState(null);
5829
6215
  const [hovered, setHovered] = useState(null);
5830
6216
  const [inspecting, setInspecting] = useState(false);
6217
+ const [popup, setPopup] = useState(null);
5831
6218
  const store = devtools?.store;
5832
6219
  const instances = useSyncExternalStore(store?.subscribe ?? (() => () => {}), () => store?.getSnapshot(), () => store?.getSnapshot())?.instances ?? [];
5833
6220
  useEffect(() => {
@@ -5886,18 +6273,85 @@ function DashboardDevtools() {
5886
6273
  }));
5887
6274
  }, [layoutTab])
5888
6275
  });
6276
+ const closePopup = useCallback(() => {
6277
+ setPopup((current) => {
6278
+ if (current && !current.closed) current.close();
6279
+ return null;
6280
+ });
6281
+ }, []);
6282
+ const openPanel = useCallback(() => {
6283
+ if (ui.mode !== "window") {
6284
+ setUi((current) => ({
6285
+ ...current,
6286
+ open: true
6287
+ }));
6288
+ return;
6289
+ }
6290
+ const opened = popup && !popup.closed ? popup : openDevtoolsWindow();
6291
+ if (opened) {
6292
+ opened.focus();
6293
+ setPopup(opened);
6294
+ setUi((current) => ({
6295
+ ...current,
6296
+ open: true
6297
+ }));
6298
+ } else setUi((current) => ({
6299
+ ...current,
6300
+ open: true,
6301
+ mode: current.dock
6302
+ }));
6303
+ }, [ui.mode, popup]);
6304
+ const closePanel = useCallback(() => {
6305
+ closePopup();
6306
+ setUi((current) => ({
6307
+ ...current,
6308
+ open: false
6309
+ }));
6310
+ }, [closePopup]);
6311
+ const toWindow = () => {
6312
+ const opened = openDevtoolsWindow();
6313
+ if (!opened) return;
6314
+ opened.focus();
6315
+ setPopup(opened);
6316
+ setUi((current) => ({
6317
+ ...current,
6318
+ open: true,
6319
+ mode: "window"
6320
+ }));
6321
+ };
6322
+ const onPopupClosed = useCallback(() => {
6323
+ setPopup(null);
6324
+ setUi((current) => ({
6325
+ ...current,
6326
+ open: false
6327
+ }));
6328
+ }, []);
6329
+ const keyActions = useRef({
6330
+ openPanel,
6331
+ closePanel,
6332
+ closePopup,
6333
+ open: ui.open,
6334
+ inspecting
6335
+ });
6336
+ keyActions.current = {
6337
+ openPanel,
6338
+ closePanel,
6339
+ closePopup,
6340
+ open: ui.open,
6341
+ inspecting
6342
+ };
5889
6343
  useEffect(() => {
5890
6344
  const onKey = (event) => {
5891
- if (event.key === "Escape" && inspecting) setInspecting(false);
6345
+ const actions = keyActions.current;
6346
+ if (event.key === "Escape" && actions.inspecting) setInspecting(false);
5892
6347
  if (!event.altKey || !event.shiftKey) return;
5893
6348
  if (event.code === "KeyB") {
5894
6349
  event.preventDefault();
5895
- setUi((current) => ({
5896
- ...current,
5897
- open: !current.open
5898
- }));
6350
+ if (actions.open) actions.closePanel();
6351
+ else actions.openPanel();
5899
6352
  } else if (event.code === "KeyF") {
5900
6353
  event.preventDefault();
6354
+ actions.closePopup();
5901
6355
  setUi((current) => ({
5902
6356
  ...current,
5903
6357
  open: true,
@@ -5908,10 +6362,13 @@ function DashboardDevtools() {
5908
6362
  setInspecting((value) => !value);
5909
6363
  }
5910
6364
  };
5911
- window.addEventListener("keydown", onKey);
5912
- return () => window.removeEventListener("keydown", onKey);
5913
- }, [inspecting]);
5914
- const reserved = ui.open && ui.mode !== "floating" ? ui.mode : null;
6365
+ const targets = popup && !popup.closed ? [window, popup] : [window];
6366
+ for (const target of targets) target.addEventListener("keydown", onKey);
6367
+ return () => {
6368
+ for (const target of targets) target.removeEventListener("keydown", onKey);
6369
+ };
6370
+ }, [popup]);
6371
+ const reserved = ui.open && ui.mode !== "floating" && ui.mode !== "window" ? ui.mode : null;
5915
6372
  const reservedSize = reserved === "bottom" ? ui.height : ui.width;
5916
6373
  const reservation = useRef(null);
5917
6374
  const reservedSizeRef = useRef(reservedSize);
@@ -5935,6 +6392,7 @@ function DashboardDevtools() {
5935
6392
  return () => window.removeEventListener("resize", onResize);
5936
6393
  }, []);
5937
6394
  if (!devtools || !store) return null;
6395
+ const inWindow = ui.mode === "window" && !!popup;
5938
6396
  const floating = ui.mode === "floating";
5939
6397
  const side = ui.mode === "left" || ui.mode === "right" ? ui.mode : null;
5940
6398
  const startDockResize = (event) => {
@@ -5971,15 +6429,21 @@ function DashboardDevtools() {
5971
6429
  })
5972
6430
  })));
5973
6431
  };
5974
- const toggleMode = () => setUi((current) => ({
5975
- ...current,
5976
- mode: current.mode === "floating" ? current.dock : "floating"
5977
- }));
5978
- const dockTo = (dock) => setUi((current) => ({
5979
- ...current,
5980
- mode: dock,
5981
- dock
5982
- }));
6432
+ const toggleMode = () => {
6433
+ closePopup();
6434
+ setUi((current) => ({
6435
+ ...current,
6436
+ mode: current.mode === "floating" ? current.dock : "floating"
6437
+ }));
6438
+ };
6439
+ const dockTo = (dock) => {
6440
+ closePopup();
6441
+ setUi((current) => ({
6442
+ ...current,
6443
+ mode: dock,
6444
+ dock
6445
+ }));
6446
+ };
5983
6447
  const clearServerCache = () => loadModel(true).then(devtools.refresh);
5984
6448
  const loading = instances.filter((item) => item.last.status === "loading").length;
5985
6449
  const errors = instances.filter((item) => item.last.status === "error").length;
@@ -6008,6 +6472,229 @@ function DashboardDevtools() {
6008
6472
  },
6009
6473
  children: /* @__PURE__ */ jsx(CircularProgress, { size: 20 })
6010
6474
  }) : content(original);
6475
+ const header = /* @__PURE__ */ jsxs(Box, {
6476
+ onMouseDown: startDrag,
6477
+ onDoubleClick: (event) => {
6478
+ if (floating && !event.target.closest("button, [role=\"tab\"]")) toggleMode();
6479
+ },
6480
+ sx: {
6481
+ display: "flex",
6482
+ alignItems: "center",
6483
+ gap: 1.5,
6484
+ px: 1.5,
6485
+ borderBottom: 1,
6486
+ borderColor: "divider",
6487
+ cursor: floating ? "move" : void 0,
6488
+ ...side && {
6489
+ flexWrap: "wrap",
6490
+ columnGap: 1,
6491
+ pt: .5
6492
+ }
6493
+ },
6494
+ children: [
6495
+ /* @__PURE__ */ jsx(Typography, {
6496
+ sx: {
6497
+ fontWeight: 800,
6498
+ color: "primary.main",
6499
+ whiteSpace: "nowrap"
6500
+ },
6501
+ children: "◆ BI Devtools"
6502
+ }),
6503
+ /* @__PURE__ */ jsx(Chip, {
6504
+ size: "small",
6505
+ label: model,
6506
+ sx: { fontFamily: MONO }
6507
+ }),
6508
+ /* @__PURE__ */ jsxs(Tabs, {
6509
+ value: ui.tab,
6510
+ onChange: (_, tab) => setTab(tab),
6511
+ variant: "scrollable",
6512
+ scrollButtons: "auto",
6513
+ sx: {
6514
+ minHeight: 40,
6515
+ ...side && {
6516
+ order: 1,
6517
+ width: "100%"
6518
+ }
6519
+ },
6520
+ children: [
6521
+ /* @__PURE__ */ jsx(Tab, {
6522
+ value: "queries",
6523
+ label: /* @__PURE__ */ jsxs(Box, {
6524
+ sx: {
6525
+ display: "flex",
6526
+ alignItems: "center",
6527
+ gap: .75
6528
+ },
6529
+ children: [
6530
+ "Consultas (",
6531
+ instances.length,
6532
+ ")",
6533
+ !!loading && /* @__PURE__ */ jsx(CircularProgress, { size: 10 }),
6534
+ !!(errors || storms) && /* @__PURE__ */ jsx(Chip, {
6535
+ size: "small",
6536
+ color: "error",
6537
+ label: errors + storms,
6538
+ sx: { height: 16 }
6539
+ })
6540
+ ]
6541
+ })
6542
+ }),
6543
+ /* @__PURE__ */ jsx(Tab, {
6544
+ value: "model",
6545
+ label: "Modelo"
6546
+ }),
6547
+ /* @__PURE__ */ jsx(Tab, {
6548
+ value: "layout",
6549
+ label: "Layout"
6550
+ }),
6551
+ /* @__PURE__ */ jsx(Tab, {
6552
+ value: "playground",
6553
+ label: "Playground"
6554
+ }),
6555
+ /* @__PURE__ */ jsx(Tab, {
6556
+ value: "regression",
6557
+ label: "Regressão"
6558
+ }),
6559
+ /* @__PURE__ */ jsx(Tab, {
6560
+ value: "dictionary",
6561
+ label: "Dicionário"
6562
+ }),
6563
+ /* @__PURE__ */ jsx(Tab, {
6564
+ value: "filters",
6565
+ label: "Filtros"
6566
+ })
6567
+ ]
6568
+ }),
6569
+ devtools.draft && /* @__PURE__ */ jsx(Chip, {
6570
+ size: "small",
6571
+ color: "warning",
6572
+ label: "rascunho aplicado"
6573
+ }),
6574
+ /* @__PURE__ */ jsxs(Box, {
6575
+ sx: {
6576
+ ml: "auto",
6577
+ display: "flex",
6578
+ alignItems: "center",
6579
+ gap: .5
6580
+ },
6581
+ children: [
6582
+ /* @__PURE__ */ jsx(Tooltip, {
6583
+ title: "Inspecionar componente (Alt+Shift+I)",
6584
+ children: /* @__PURE__ */ jsx(IconButton, {
6585
+ size: "small",
6586
+ color: inspecting ? "primary" : "default",
6587
+ onClick: () => setInspecting((value) => !value),
6588
+ "aria-label": "Inspecionar componente",
6589
+ children: "⌖"
6590
+ })
6591
+ }),
6592
+ DOCK_SIDES.map(({ side: dock, icon, label }) => /* @__PURE__ */ jsx(Tooltip, {
6593
+ title: `${label} (ocupa espaço na página)`,
6594
+ children: /* @__PURE__ */ jsx(IconButton, {
6595
+ size: "small",
6596
+ color: ui.mode === dock ? "primary" : "default",
6597
+ onClick: () => dockTo(dock),
6598
+ "aria-label": label,
6599
+ children: icon
6600
+ })
6601
+ }, dock)),
6602
+ /* @__PURE__ */ jsx(Tooltip, {
6603
+ title: "Janela flutuante, por cima da página (Alt+Shift+F) · arraste pelo cabeçalho",
6604
+ children: /* @__PURE__ */ jsx(IconButton, {
6605
+ size: "small",
6606
+ color: floating ? "primary" : "default",
6607
+ onClick: toggleMode,
6608
+ "aria-label": "Devtools flutuante",
6609
+ children: "⧉"
6610
+ })
6611
+ }),
6612
+ /* @__PURE__ */ jsx(Tooltip, {
6613
+ title: "Abrir numa janela separada do navegador (ex.: em outro monitor)",
6614
+ children: /* @__PURE__ */ jsx(IconButton, {
6615
+ size: "small",
6616
+ color: inWindow ? "primary" : "default",
6617
+ onClick: inWindow ? () => popup?.focus() : toWindow,
6618
+ "aria-label": "Devtools em janela separada",
6619
+ children: "↗"
6620
+ })
6621
+ }),
6622
+ /* @__PURE__ */ jsx(Tooltip, {
6623
+ title: "Fechar (Alt+Shift+B)",
6624
+ children: /* @__PURE__ */ jsx(IconButton, {
6625
+ size: "small",
6626
+ onClick: closePanel,
6627
+ "aria-label": "Fechar devtools",
6628
+ children: "✕"
6629
+ })
6630
+ })
6631
+ ]
6632
+ })
6633
+ ]
6634
+ });
6635
+ const body = /* @__PURE__ */ jsxs(Box, {
6636
+ sx: {
6637
+ flex: 1,
6638
+ minHeight: 0
6639
+ },
6640
+ children: [
6641
+ ui.tab === "queries" && /* @__PURE__ */ jsx(QueriesPanel, {
6642
+ devtools,
6643
+ capabilities,
6644
+ selected,
6645
+ onSelect: setSelected,
6646
+ onHover: setHovered,
6647
+ onClearServerCache: clearServerCache
6648
+ }),
6649
+ ui.tab === "model" && needsModel((registered) => /* @__PURE__ */ jsx(ModelPanel, {
6650
+ devtools,
6651
+ original: registered,
6652
+ capabilities,
6653
+ tables: tables ?? [],
6654
+ onTry: (selection) => {
6655
+ setPlayground(selection);
6656
+ setTab("playground");
6657
+ },
6658
+ onSaved: () => setTimeout(() => loadModel(), 1e3)
6659
+ })),
6660
+ ui.tab === "layout" && needsModel((registered) => /* @__PURE__ */ jsx(LayoutPanel, {
6661
+ devtools,
6662
+ model: devtools.draft ?? registered,
6663
+ capabilities,
6664
+ picked: pickedItem,
6665
+ onHover: setHovered
6666
+ })),
6667
+ ui.tab === "playground" && needsModel((registered) => /* @__PURE__ */ jsx(PlaygroundPanel, {
6668
+ model: devtools.draft ?? registered,
6669
+ selection: playground,
6670
+ onSelectionChange: setPlayground
6671
+ })),
6672
+ ui.tab === "regression" && /* @__PURE__ */ jsx(RegressionPanel, {
6673
+ devtools,
6674
+ capabilities
6675
+ }),
6676
+ ui.tab === "dictionary" && /* @__PURE__ */ jsxs(Box, {
6677
+ sx: {
6678
+ p: 1.5,
6679
+ overflow: "auto",
6680
+ height: "100%"
6681
+ },
6682
+ children: [/* @__PURE__ */ jsxs(Typography, {
6683
+ sx: {
6684
+ fontSize: 12,
6685
+ color: "text.secondary",
6686
+ mb: 1.5
6687
+ },
6688
+ children: [
6689
+ "O mesmo ",
6690
+ /* @__PURE__ */ jsx("code", { children: "<ModelDictionary />" }),
6691
+ " pode ir numa página do dashboard como documentação para quem usa. Edite rótulos e descrições na aba Modelo."
6692
+ ]
6693
+ }), /* @__PURE__ */ jsx(ModelDictionary, {})]
6694
+ }),
6695
+ ui.tab === "filters" && /* @__PURE__ */ jsx(FiltersPanel, { devtools })
6696
+ ]
6697
+ });
6011
6698
  return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsxs(ThemeProvider, {
6012
6699
  theme,
6013
6700
  children: [
@@ -6016,7 +6703,26 @@ function DashboardDevtools() {
6016
6703
  label: hoveredLabel,
6017
6704
  zIndex: 1251
6018
6705
  }),
6019
- ui.open ? /* @__PURE__ */ jsxs(Paper, {
6706
+ ui.open && inWindow && popup && /* @__PURE__ */ jsx(DevtoolsWindow, {
6707
+ popup,
6708
+ title: `BI Devtools · ${model}`,
6709
+ theme,
6710
+ onClosed: onPopupClosed,
6711
+ children: /* @__PURE__ */ jsxs(Paper, {
6712
+ ...devtoolsMarker,
6713
+ square: true,
6714
+ elevation: 0,
6715
+ sx: {
6716
+ height: "100%",
6717
+ display: "flex",
6718
+ flexDirection: "column",
6719
+ color: "text.primary",
6720
+ overflow: "hidden"
6721
+ },
6722
+ children: [header, body]
6723
+ })
6724
+ }),
6725
+ ui.open && !inWindow ? /* @__PURE__ */ jsxs(Paper, {
6020
6726
  ...devtoolsMarker,
6021
6727
  square: !floating,
6022
6728
  elevation: floating ? 16 : 12,
@@ -6081,232 +6787,15 @@ function DashboardDevtools() {
6081
6787
  cursor: "ns-resize"
6082
6788
  }
6083
6789
  }),
6084
- /* @__PURE__ */ jsxs(Box, {
6085
- onMouseDown: startDrag,
6086
- onDoubleClick: (event) => {
6087
- if (floating && !event.target.closest("button, [role=\"tab\"]")) toggleMode();
6088
- },
6089
- sx: {
6090
- display: "flex",
6091
- alignItems: "center",
6092
- gap: 1.5,
6093
- px: 1.5,
6094
- borderBottom: 1,
6095
- borderColor: "divider",
6096
- cursor: floating ? "move" : void 0,
6097
- ...side && {
6098
- flexWrap: "wrap",
6099
- columnGap: 1,
6100
- pt: .5
6101
- }
6102
- },
6103
- children: [
6104
- /* @__PURE__ */ jsx(Typography, {
6105
- sx: {
6106
- fontWeight: 800,
6107
- color: "primary.main",
6108
- whiteSpace: "nowrap"
6109
- },
6110
- children: "◆ BI Devtools"
6111
- }),
6112
- /* @__PURE__ */ jsx(Chip, {
6113
- size: "small",
6114
- label: model,
6115
- sx: { fontFamily: MONO }
6116
- }),
6117
- /* @__PURE__ */ jsxs(Tabs, {
6118
- value: ui.tab,
6119
- onChange: (_, tab) => setTab(tab),
6120
- variant: "scrollable",
6121
- scrollButtons: "auto",
6122
- sx: {
6123
- minHeight: 40,
6124
- ...side && {
6125
- order: 1,
6126
- width: "100%"
6127
- }
6128
- },
6129
- children: [
6130
- /* @__PURE__ */ jsx(Tab, {
6131
- value: "queries",
6132
- label: /* @__PURE__ */ jsxs(Box, {
6133
- sx: {
6134
- display: "flex",
6135
- alignItems: "center",
6136
- gap: .75
6137
- },
6138
- children: [
6139
- "Consultas (",
6140
- instances.length,
6141
- ")",
6142
- !!loading && /* @__PURE__ */ jsx(CircularProgress, { size: 10 }),
6143
- !!(errors || storms) && /* @__PURE__ */ jsx(Chip, {
6144
- size: "small",
6145
- color: "error",
6146
- label: errors + storms,
6147
- sx: { height: 16 }
6148
- })
6149
- ]
6150
- })
6151
- }),
6152
- /* @__PURE__ */ jsx(Tab, {
6153
- value: "model",
6154
- label: "Modelo"
6155
- }),
6156
- /* @__PURE__ */ jsx(Tab, {
6157
- value: "layout",
6158
- label: "Layout"
6159
- }),
6160
- /* @__PURE__ */ jsx(Tab, {
6161
- value: "playground",
6162
- label: "Playground"
6163
- }),
6164
- /* @__PURE__ */ jsx(Tab, {
6165
- value: "regression",
6166
- label: "Regressão"
6167
- }),
6168
- /* @__PURE__ */ jsx(Tab, {
6169
- value: "dictionary",
6170
- label: "Dicionário"
6171
- }),
6172
- /* @__PURE__ */ jsx(Tab, {
6173
- value: "filters",
6174
- label: "Filtros"
6175
- })
6176
- ]
6177
- }),
6178
- devtools.draft && /* @__PURE__ */ jsx(Chip, {
6179
- size: "small",
6180
- color: "warning",
6181
- label: "rascunho aplicado"
6182
- }),
6183
- /* @__PURE__ */ jsxs(Box, {
6184
- sx: {
6185
- ml: "auto",
6186
- display: "flex",
6187
- alignItems: "center",
6188
- gap: .5
6189
- },
6190
- children: [
6191
- /* @__PURE__ */ jsx(Tooltip, {
6192
- title: "Inspecionar componente (Alt+Shift+I)",
6193
- children: /* @__PURE__ */ jsx(IconButton, {
6194
- size: "small",
6195
- color: inspecting ? "primary" : "default",
6196
- onClick: () => setInspecting((value) => !value),
6197
- "aria-label": "Inspecionar componente",
6198
- children: "⌖"
6199
- })
6200
- }),
6201
- DOCK_SIDES.map(({ side: dock, icon, label }) => /* @__PURE__ */ jsx(Tooltip, {
6202
- title: `${label} (ocupa espaço na página)`,
6203
- children: /* @__PURE__ */ jsx(IconButton, {
6204
- size: "small",
6205
- color: ui.mode === dock ? "primary" : "default",
6206
- onClick: () => dockTo(dock),
6207
- "aria-label": label,
6208
- children: icon
6209
- })
6210
- }, dock)),
6211
- /* @__PURE__ */ jsx(Tooltip, {
6212
- title: "Janela flutuante, por cima da página (Alt+Shift+F) · arraste pelo cabeçalho",
6213
- children: /* @__PURE__ */ jsx(IconButton, {
6214
- size: "small",
6215
- color: floating ? "primary" : "default",
6216
- onClick: toggleMode,
6217
- "aria-label": "Devtools flutuante",
6218
- children: "⧉"
6219
- })
6220
- }),
6221
- /* @__PURE__ */ jsx(Tooltip, {
6222
- title: "Fechar (Alt+Shift+B)",
6223
- children: /* @__PURE__ */ jsx(IconButton, {
6224
- size: "small",
6225
- onClick: () => setUi((current) => ({
6226
- ...current,
6227
- open: false
6228
- })),
6229
- "aria-label": "Fechar devtools",
6230
- children: "✕"
6231
- })
6232
- })
6233
- ]
6234
- })
6235
- ]
6236
- }),
6237
- /* @__PURE__ */ jsxs(Box, {
6238
- sx: {
6239
- flex: 1,
6240
- minHeight: 0
6241
- },
6242
- children: [
6243
- ui.tab === "queries" && /* @__PURE__ */ jsx(QueriesPanel, {
6244
- devtools,
6245
- capabilities,
6246
- selected,
6247
- onSelect: setSelected,
6248
- onHover: setHovered,
6249
- onClearServerCache: clearServerCache
6250
- }),
6251
- ui.tab === "model" && needsModel((registered) => /* @__PURE__ */ jsx(ModelPanel, {
6252
- devtools,
6253
- original: registered,
6254
- capabilities,
6255
- tables: tables ?? [],
6256
- onTry: (selection) => {
6257
- setPlayground(selection);
6258
- setTab("playground");
6259
- },
6260
- onSaved: () => setTimeout(() => loadModel(), 1e3)
6261
- })),
6262
- ui.tab === "layout" && needsModel((registered) => /* @__PURE__ */ jsx(LayoutPanel, {
6263
- devtools,
6264
- model: devtools.draft ?? registered,
6265
- capabilities,
6266
- picked: pickedItem,
6267
- onHover: setHovered
6268
- })),
6269
- ui.tab === "playground" && needsModel((registered) => /* @__PURE__ */ jsx(PlaygroundPanel, {
6270
- model: devtools.draft ?? registered,
6271
- selection: playground,
6272
- onSelectionChange: setPlayground
6273
- })),
6274
- ui.tab === "regression" && /* @__PURE__ */ jsx(RegressionPanel, {
6275
- devtools,
6276
- capabilities
6277
- }),
6278
- ui.tab === "dictionary" && /* @__PURE__ */ jsxs(Box, {
6279
- sx: {
6280
- p: 1.5,
6281
- overflow: "auto",
6282
- height: "100%"
6283
- },
6284
- children: [/* @__PURE__ */ jsxs(Typography, {
6285
- sx: {
6286
- fontSize: 12,
6287
- color: "text.secondary",
6288
- mb: 1.5
6289
- },
6290
- children: [
6291
- "O mesmo ",
6292
- /* @__PURE__ */ jsx("code", { children: "<ModelDictionary />" }),
6293
- " pode ir numa página do dashboard como documentação para quem usa. Edite rótulos e descrições na aba Modelo."
6294
- ]
6295
- }), /* @__PURE__ */ jsx(ModelDictionary, {})]
6296
- }),
6297
- ui.tab === "filters" && /* @__PURE__ */ jsx(FiltersPanel, { devtools })
6298
- ]
6299
- })
6790
+ header,
6791
+ body
6300
6792
  ]
6301
6793
  }) : /* @__PURE__ */ jsx(ToggleButton$1, {
6302
6794
  draft: !!devtools.draft,
6303
6795
  loading,
6304
6796
  problems: errors + storms,
6305
6797
  inspecting,
6306
- onOpen: () => setUi((current) => ({
6307
- ...current,
6308
- open: true
6309
- }))
6798
+ onOpen: inWindow ? () => popup?.focus() : openPanel
6310
6799
  }),
6311
6800
  inspecting && /* @__PURE__ */ jsx(Box, {
6312
6801
  ...devtoolsMarker,
@@ -6394,4 +6883,4 @@ function ToggleButton$1({ draft, loading, problems, inspecting, onOpen }) {
6394
6883
  //#endregion
6395
6884
  export { DashboardDevtools as default };
6396
6885
 
6397
- //# sourceMappingURL=DashboardDevtools-5wh3brAT.js.map
6886
+ //# sourceMappingURL=DashboardDevtools-C4E9afPT.js.map