@12-apps/ui 1.6.3 → 1.8.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.
Files changed (39) hide show
  1. package/package.json +4 -4
  2. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +1 -1
  3. package/src/components/data-display/DataViews/RESPONSIVE-BEHAVIOUR.md +183 -0
  4. package/src/components/data-display/DataViews/base-card.tsx +93 -23
  5. package/src/components/data-display/DataViews/base-list-card-slots.tsx +256 -0
  6. package/src/components/data-display/DataViews/base-list-card.tsx +352 -0
  7. package/src/components/data-display/DataViews/card-surface.ts +297 -0
  8. package/src/components/data-display/DataViews/data-views-bar-controls.tsx +119 -0
  9. package/src/components/data-display/DataViews/data-views-display-panel.tsx +48 -22
  10. package/src/components/data-display/DataViews/data-views-display-tabs.tsx +64 -31
  11. package/src/components/data-display/DataViews/data-views-drag.tsx +135 -0
  12. package/src/components/data-display/DataViews/data-views-export.tsx +2 -1
  13. package/src/components/data-display/DataViews/data-views-glyphs.tsx +16 -0
  14. package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +5 -1
  15. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +20 -14
  16. package/src/components/data-display/DataViews/data-views-inline-bar.tsx +134 -155
  17. package/src/components/data-display/DataViews/data-views-layout-context.tsx +21 -0
  18. package/src/components/data-display/DataViews/data-views-more-fields.tsx +291 -0
  19. package/src/components/data-display/DataViews/data-views-more-filters.tsx +151 -199
  20. package/src/components/data-display/DataViews/data-views-overflow-costs.ts +100 -0
  21. package/src/components/data-display/DataViews/data-views-overflow.ts +89 -73
  22. package/src/components/data-display/DataViews/data-views-preset-chips.tsx +100 -0
  23. package/src/components/data-display/DataViews/data-views-range-pill.tsx +27 -11
  24. package/src/components/data-display/DataViews/data-views-range-presets.ts +121 -0
  25. package/src/components/data-display/DataViews/data-views-range-types.ts +94 -0
  26. package/src/components/data-display/DataViews/data-views-scope-tabs.tsx +57 -0
  27. package/src/components/data-display/DataViews/data-views-scopes.tsx +2 -2
  28. package/src/components/data-display/DataViews/data-views-search.tsx +19 -1
  29. package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +4 -0
  30. package/src/components/data-display/DataViews/data-views-toolbar.tsx +31 -21
  31. package/src/components/data-display/DataViews/data-views-types.ts +14 -47
  32. package/src/components/data-display/DataViews/index.ts +8 -0
  33. package/src/components/data-display/DataViews/list-card-rails.tsx +141 -0
  34. package/src/components/data-display/DataViews/pedidos.server.ts +118 -0
  35. package/src/components/data-display/DataViews/use-data-views-state.ts +1 -1
  36. package/src/components/data-display/DescriptionItem/DescriptionItem.tsx +26 -3
  37. package/src/components/layout/ContentToolbar/ContentToolbar.tsx +16 -1
  38. package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +9 -1
  39. package/src/components/layout/ContentToolbar/MultiSelectTriggers.tsx +39 -1
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ /**
4
+ * THE TWO BARE AFFORDANCES ON THE FILTER BAR.
5
+ *
6
+ * Neither opens anything, which is what sets them apart from every other
7
+ * control on that row and why they are styled flat rather than as outlined
8
+ * pills: one clears the filters, one leaves a search takeover. Giving them a
9
+ * pill's weight made "Limpar" read as a sixth filter.
10
+ *
11
+ * Their own module because `data-views-inline-bar` outgrew the file-size gate.
12
+ */
13
+ import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
14
+ import FilterAltOffRoundedIcon from "@mui/icons-material/FilterAltOffRounded";
15
+ import { Tooltip } from "@mui/material";
16
+
17
+ import { Box } from "../../../mui/Box";
18
+
19
+ /**
20
+ * "Limpar" — one gesture back to the unfiltered list.
21
+ *
22
+ * Present ONLY while something is applied, and last in the cluster, because it
23
+ * is destructive and must never be where a filter control was a moment ago.
24
+ * Each pill already carries its own ✕; this is the other job — six applied
25
+ * filters is six ✕ hunts, and the operator who wants "show me everything
26
+ * again" has no way to say it.
27
+ *
28
+ * Loses its label on the same rung Exibir/Exportar lose theirs, and keeps a
29
+ * tooltip when it does: it is one of the two controls (with the search) whose
30
+ * icon has to carry the whole meaning on a phone.
31
+ */
32
+ export function ClearAllControl({
33
+ onClearAll,
34
+ compact,
35
+ testIdPrefix,
36
+ }: {
37
+ onClearAll: () => void;
38
+ compact: boolean;
39
+ testIdPrefix: string;
40
+ }): React.JSX.Element {
41
+ // No border, no fill. Every other control on this row is an outlined pill
42
+ // because it OPENS something; this one is an escape hatch, and giving it the
43
+ // same weight made it read as a sixth filter. Quiet until hovered.
44
+ const button = (
45
+ <Box
46
+ component="button"
47
+ type="button"
48
+ onClick={onClearAll}
49
+ data-testid={`${testIdPrefix}-clear-all`}
50
+ aria-label="Limpar filtros"
51
+ sx={{
52
+ display: "inline-flex",
53
+ alignItems: "center",
54
+ gap: 0.5,
55
+ height: 34,
56
+ px: compact ? 0.75 : 1,
57
+ border: 0,
58
+ borderRadius: 1,
59
+ bgcolor: "transparent",
60
+ cursor: "pointer",
61
+ font: "inherit",
62
+ fontSize: "0.8125rem",
63
+ whiteSpace: "nowrap",
64
+ color: "text.secondary",
65
+ "&:hover": { color: "text.primary", bgcolor: "action.hover" },
66
+ }}
67
+ >
68
+ <FilterAltOffRoundedIcon sx={{ fontSize: 16 }} />
69
+ {!compact && <Box component="span">Limpar</Box>}
70
+ </Box>
71
+ );
72
+ return compact ? <Tooltip title="Limpar filtros">{button}</Tooltip> : button;
73
+ }
74
+
75
+ /**
76
+ * The way out of a search takeover.
77
+ *
78
+ * A phone has no Escape key, and without this the takeover is a one-way door
79
+ * onto a bar with no filters on it.
80
+
81
+ /**
82
+ * The way out of a search takeover.
83
+ *
84
+ * A phone has no Escape key, and without this the takeover is a one-way door
85
+ * onto a bar with no filters on it.
86
+ */
87
+ export function CloseSearchControl({
88
+ onClose,
89
+ testIdPrefix,
90
+ }: {
91
+ onClose: () => void;
92
+ testIdPrefix: string;
93
+ }): React.JSX.Element {
94
+ return (
95
+ <Tooltip title="Fechar busca">
96
+ <Box
97
+ component="button"
98
+ type="button"
99
+ onClick={onClose}
100
+ aria-label="Fechar busca"
101
+ data-testid={`${testIdPrefix}-search-close`}
102
+ sx={{
103
+ display: "inline-flex",
104
+ alignItems: "center",
105
+ height: 34,
106
+ px: 0.75,
107
+ border: 0,
108
+ borderRadius: 1,
109
+ bgcolor: "transparent",
110
+ cursor: "pointer",
111
+ color: "text.secondary",
112
+ "&:hover": { color: "text.primary", bgcolor: "action.hover" },
113
+ }}
114
+ >
115
+ <CloseRoundedIcon sx={{ fontSize: 18 }} />
116
+ </Box>
117
+ </Tooltip>
118
+ );
119
+ }
@@ -244,6 +244,48 @@ function ViewsBody({
244
244
  return <ViewsList handlers={nav} onOpenActions={onOpenActions} testIdPrefix={testIdPrefix} />;
245
245
  }
246
246
 
247
+ /** The "Exibir" button, with the dot that says the view has unsaved changes. */
248
+ function DisplayTrigger({
249
+ dirty,
250
+ compact,
251
+ onOpen,
252
+ testIdPrefix,
253
+ }: {
254
+ dirty: boolean;
255
+ compact: boolean;
256
+ onOpen: (anchor: HTMLElement) => void;
257
+ testIdPrefix: string;
258
+ }): React.JSX.Element {
259
+ return (
260
+ <Button
261
+ variant="outline"
262
+ size="sm"
263
+ color="neutral"
264
+ onClick={(event) => onOpen(event.currentTarget as HTMLElement)}
265
+ dataTestId={`${testIdPrefix}-display-trigger`}
266
+ aria-label="Exibir"
267
+ title={compact ? "Exibir" : undefined}
268
+ >
269
+ <Box component="span" sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}>
270
+ <TuneRoundedIcon fontSize="small" />
271
+ {!compact && (
272
+ <Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
273
+ Exibir
274
+ </Box>
275
+ )}
276
+ {dirty && (
277
+ <Box component="span" sx={{ height: 6, width: 6, borderRadius: "50%", bgcolor: "primary.main" }} />
278
+ )}
279
+ {/* The chevron goes with the label. Once the control is a bare icon it
280
+ is already unmistakably a button, and on the rung where that happens
281
+ the ~24px it costs is the difference between a row that fits and one
282
+ that scrolls. */}
283
+ {!compact && <KeyboardArrowDownRoundedIcon fontSize="small" />}
284
+ </Box>
285
+ </Button>
286
+ );
287
+ }
288
+
247
289
  /**
248
290
  * The toolbar's "Exibir" control: one button, one popover, three tabs, and the
249
291
  * view bracketing all of it.
@@ -271,28 +313,12 @@ export function DataViewsDisplayPanel<T extends Record<string, unknown>>({
271
313
  };
272
314
  return (
273
315
  <>
274
- <Button
275
- variant="outline"
276
- size="sm"
277
- color="neutral"
278
- onClick={(event) => setAnchor(event.currentTarget as HTMLElement)}
279
- dataTestId={`${testIdPrefix}-display-trigger`}
280
- aria-label="Exibir"
281
- title={compact ? "Exibir" : undefined}
282
- >
283
- <Box component="span" sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}>
284
- <TuneRoundedIcon fontSize="small" />
285
- {!compact && (
286
- <Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
287
- Exibir
288
- </Box>
289
- )}
290
- {view?.dirty && (
291
- <Box component="span" sx={{ height: 6, width: 6, borderRadius: "50%", bgcolor: "primary.main" }} />
292
- )}
293
- <KeyboardArrowDownRoundedIcon fontSize="small" />
294
- </Box>
295
- </Button>
316
+ <DisplayTrigger
317
+ dirty={Boolean(view?.dirty)}
318
+ compact={Boolean(compact)}
319
+ onOpen={setAnchor}
320
+ testIdPrefix={testIdPrefix}
321
+ />
296
322
  <Popover
297
323
  open={Boolean(anchor)}
298
324
  anchorEl={anchor}
@@ -9,6 +9,7 @@ import { Text } from "../../typography/Text";
9
9
 
10
10
  import {
11
11
  BoardGlyph,
12
+ ColumnsGlyph,
12
13
  GridGlyph,
13
14
  ListGlyph,
14
15
  RowsGlyph,
@@ -219,15 +220,52 @@ const LAYOUT_HINTS: Record<DataViewsLayout, string> = {
219
220
  board: "Colunas por etapa, para acompanhar o que está onde.",
220
221
  };
221
222
 
223
+ /**
224
+ * What density MEANS in each layout — same three values, three different
225
+ * questions. A table's is how tall its rows are, a card grid's is how many fit
226
+ * on a line, and a board's is how wide its columns are. The heading and the
227
+ * tile labels change with the layout because "Alta" over a board is meaningless
228
+ * where "Larga" is not.
229
+ */
230
+ const DENSITY_HEADINGS: Record<DataViewsLayout, string> = {
231
+ table: "Altura das linhas",
232
+ list: "Altura das linhas",
233
+ cards: "Cards por linha",
234
+ board: "Largura das colunas",
235
+ };
236
+
237
+ const DENSITY_LABELS: Record<DataViewsLayout, Record<DataViewsDensity, string>> = {
238
+ table: { compact: "Baixa", cozy: "Média", comfortable: "Alta" },
239
+ list: { compact: "Baixa", cozy: "Média", comfortable: "Alta" },
240
+ cards: { compact: "Muitos", cozy: "Médio", comfortable: "Poucos" },
241
+ board: { compact: "Estreita", cozy: "Média", comfortable: "Larga" },
242
+ };
243
+
222
244
  /** The density tiles, phrased for whichever layout is on screen. */
223
- function densityTiles(cards: boolean): { value: DataViewsDensity; label: string; gap: number; columns: number }[] {
245
+ function densityTiles(
246
+ layout: DataViewsLayout,
247
+ ): { value: DataViewsDensity; label: string; gap: number; columns: number }[] {
248
+ const labels = DENSITY_LABELS[layout];
224
249
  return [
225
- { value: "compact", label: cards ? "Muitos" : "Baixa", gap: 2, columns: 4 },
226
- { value: "cozy", label: cards ? "Médio" : "Média", gap: 5, columns: 3 },
227
- { value: "comfortable", label: cards ? "Poucos" : "Alta", gap: 8, columns: 2 },
250
+ { value: "compact", label: labels.compact, gap: 2, columns: 4 },
251
+ { value: "cozy", label: labels.cozy, gap: 5, columns: 3 },
252
+ { value: "comfortable", label: labels.comfortable, gap: 8, columns: 2 },
228
253
  ];
229
254
  }
230
255
 
256
+ /** The preview for one density tile, in the terms that layout's density is in. */
257
+ function densityGlyph(
258
+ layout: DataViewsLayout,
259
+ tile: { gap: number; columns: number },
260
+ active: boolean,
261
+ ): React.JSX.Element {
262
+ if (layout === "cards") return <GridGlyph n={tile.columns} active={active} />;
263
+ // A wider column means FEWER of them across the same board, so the
264
+ // comfortable end draws two fat columns and the compact end four thin ones.
265
+ if (layout === "board") return <ColumnsGlyph n={tile.columns} active={active} />;
266
+ return <RowsGlyph gap={tile.gap} active={active} />;
267
+ }
268
+
231
269
  /** The glyph for a layout tile. */
232
270
  function layoutGlyph(layout: DataViewsLayout, active: boolean): React.JSX.Element {
233
271
  if (layout === "table") return <TableGlyph active={active} />;
@@ -257,7 +295,6 @@ export function DisplayTab({ testIdPrefix }: { testIdPrefix: string }): React.JS
257
295
  ...(canUseCards ? (["cards"] as const) : []),
258
296
  ...(canUseBoard ? (["board"] as const) : []),
259
297
  ];
260
- const cards = layout === "cards";
261
298
  return (
262
299
  <Box sx={{ p: 1 }}>
263
300
  <Text variant="caption" as="p">
@@ -290,32 +327,28 @@ export function DisplayTab({ testIdPrefix }: { testIdPrefix: string }): React.JS
290
327
  {LAYOUT_HINTS[layout]}
291
328
  </Box>
292
329
  </Text>
293
- {layout !== "board" && (
294
- <>
295
- <Text variant="caption" as="p">
296
- <Box component="span" sx={{ px: 0.5, color: "text.secondary" }}>
297
- {cards ? "Cards por linha" : "Altura das linhas"}
298
- </Box>
299
- </Text>
300
- <Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 0.75, mt: 0.5 }}>
301
- {densityTiles(cards).map((tile) => (
302
- <Tile
303
- key={tile.value}
304
- active={density === tile.value}
305
- label={tile.label}
306
- onClick={() => setDensity(tile.value)}
307
- testId={`${testIdPrefix}-density-${tile.value}`}
308
- >
309
- {cards ? (
310
- <GridGlyph n={tile.columns} active={density === tile.value} />
311
- ) : (
312
- <RowsGlyph gap={tile.gap} active={density === tile.value} />
313
- )}
314
- </Tile>
315
- ))}
316
- </Box>
317
- </>
318
- )}
330
+ {/* Every layout gets this, the board included. It used to be hidden on
331
+ Quadro, which left the format tile switching to a view with no sizing
332
+ control at all — and the section disappearing on select reads as a
333
+ control that broke rather than one that does not apply. */}
334
+ <Text variant="caption" as="p">
335
+ <Box component="span" sx={{ px: 0.5, color: "text.secondary" }}>
336
+ {DENSITY_HEADINGS[layout]}
337
+ </Box>
338
+ </Text>
339
+ <Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 0.75, mt: 0.5 }}>
340
+ {densityTiles(layout).map((tile) => (
341
+ <Tile
342
+ key={tile.value}
343
+ active={density === tile.value}
344
+ label={tile.label}
345
+ onClick={() => setDensity(tile.value)}
346
+ testId={`${testIdPrefix}-density-${tile.value}`}
347
+ >
348
+ {densityGlyph(layout, tile, density === tile.value)}
349
+ </Tile>
350
+ ))}
351
+ </Box>
319
352
  {!canUseBoard && (
320
353
  <Text variant="caption" as="p">
321
354
  <Box component="span" sx={{ px: 0.5, pt: 1.25, display: "block", color: "text.disabled" }}>
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, type ReactNode } from "react";
4
+
5
+ import { Box } from "../../../mui/Box";
6
+
7
+ /**
8
+ * DRAGGING, OWNED BY THE CONTAINER AND NOT BY THE CARD.
9
+ *
10
+ * A card cannot know whether it is draggable. The same `BaseListCard` renders
11
+ * inside a reorderable queue, a read-only report and a board column that moves
12
+ * items between columns — and only the thing holding them knows which. So the
13
+ * container publishes a drag context and every card inside it becomes
14
+ * draggable; a card with no container above it renders exactly as before, with
15
+ * no handle and no listeners.
16
+ *
17
+ * This is a SEAM, not a drag implementation. It carries whatever props the
18
+ * host's library hands out — dnd-kit's `attributes`/`listeners`, react-dnd's
19
+ * connector output, or plain HTML5 `draggable` + `onDragStart` — because
20
+ * picking one of those for every consumer of `@12-apps/ui` is a decision this
21
+ * package has no business making. There is no drag dependency here, and adding
22
+ * one would force it on every app that installs the library.
23
+ */
24
+
25
+ /** The props a container hands to one item. Deliberately untyped-ish: each DnD library has its own shape. */
26
+ export type DragItemProps = Record<string, unknown>;
27
+
28
+ export interface DragContainerValue {
29
+ /**
30
+ * Props for the item's ROOT — the element that moves. dnd-kit wants its
31
+ * `setNodeRef` and transform style here; HTML5 wants `draggable` and
32
+ * `onDragStart`.
33
+ */
34
+ itemProps: (id: string | number) => DragItemProps;
35
+ /**
36
+ * Props for the HANDLE alone, when the container wants drags to start only
37
+ * from the grip.
38
+ *
39
+ * Whole-card dragging breaks the two things a card must still do: selecting
40
+ * text in it, and hitting the checkbox without the row sliding away. When a
41
+ * container returns handle props, the card puts them on the grip and leaves
42
+ * the rest of itself alone.
43
+ */
44
+ handleProps?: (id: string | number) => DragItemProps;
45
+ /** The id currently being dragged, so its card can dim itself. */
46
+ activeId?: string | number | null;
47
+ }
48
+
49
+ const DragContainerContext = createContext<DragContainerValue | null>(null);
50
+
51
+ /**
52
+ * Marks everything inside as draggable. Cards read this; nothing else does.
53
+ */
54
+ export function DragContainerProvider({
55
+ value,
56
+ children,
57
+ }: {
58
+ value: DragContainerValue;
59
+ children: ReactNode;
60
+ }): React.JSX.Element {
61
+ return <DragContainerContext.Provider value={value}>{children}</DragContainerContext.Provider>;
62
+ }
63
+
64
+ /**
65
+ * What one card needs to know about dragging.
66
+ *
67
+ * Returns the inert answer when there is no container OR no `dragId` — a card
68
+ * that forgot its id must not render a handle that does nothing, which is worse
69
+ * than no handle at all.
70
+ */
71
+ export function useDragItem(dragId: string | number | undefined): {
72
+ draggable: boolean;
73
+ itemProps: DragItemProps;
74
+ handleProps: DragItemProps | undefined;
75
+ dragging: boolean;
76
+ } {
77
+ const container = useContext(DragContainerContext);
78
+ if (!container || dragId === undefined) {
79
+ return { draggable: false, itemProps: {}, handleProps: undefined, dragging: false };
80
+ }
81
+ return {
82
+ draggable: true,
83
+ itemProps: container.itemProps(dragId),
84
+ handleProps: container.handleProps?.(dragId),
85
+ dragging: container.activeId === dragId,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * The grip. Six dots, the convention everywhere, drawn rather than imported so
91
+ * this costs no icon dependency.
92
+ *
93
+ * `cursor: grab` on the handle only when the container gates drags to it —
94
+ * otherwise the whole card carries the cursor and the handle is just an
95
+ * affordance saying so.
96
+ */
97
+ export function DragHandle({
98
+ handleProps,
99
+ gated,
100
+ testId,
101
+ }: {
102
+ handleProps: DragItemProps | undefined;
103
+ gated: boolean;
104
+ testId?: string;
105
+ }): React.JSX.Element {
106
+ return (
107
+ <Box
108
+ component="span"
109
+ aria-hidden={handleProps ? undefined : true}
110
+ data-testid={testId}
111
+ {...handleProps}
112
+ sx={{
113
+ display: "inline-grid",
114
+ gridTemplateColumns: "repeat(2, 3px)",
115
+ gap: "2px",
116
+ flexShrink: 0,
117
+ p: 0.5,
118
+ borderRadius: 1,
119
+ color: "text.disabled",
120
+ cursor: gated ? "grab" : "inherit",
121
+ touchAction: "none",
122
+ "&:active": { cursor: gated ? "grabbing" : "inherit" },
123
+ "&:hover": { color: "text.secondary" },
124
+ }}
125
+ >
126
+ {Array.from({ length: 6 }).map((_, index) => (
127
+ <Box
128
+ key={index}
129
+ component="span"
130
+ sx={{ width: 3, height: 3, borderRadius: "50%", bgcolor: "currentColor" }}
131
+ />
132
+ ))}
133
+ </Box>
134
+ );
135
+ }
@@ -116,7 +116,8 @@ function ExportTrigger({
116
116
  Exportar
117
117
  </Box>
118
118
  )}
119
- <KeyboardArrowDownRoundedIcon fontSize="small" />
119
+ {/* Dropped with the label — see the note on the Exibir trigger. */}
120
+ {!compact && <KeyboardArrowDownRoundedIcon fontSize="small" />}
120
121
  </Box>
121
122
  </Button>
122
123
  );
@@ -87,6 +87,22 @@ export function GridGlyph({ n = 3, active }: { n?: number; active: boolean }): R
87
87
  );
88
88
  }
89
89
 
90
+ /**
91
+ * Full-height columns, `n` of them — the board's DENSITY, where fewer, wider
92
+ * columns is the comfortable end. Distinct from {@link BoardGlyph}, which
93
+ * stands for the board format itself and shows cards stacked at differing
94
+ * depths; this one is about the columns' width, so the bars are solid.
95
+ */
96
+ export function ColumnsGlyph({ n, active }: { n: number; active: boolean }): React.JSX.Element {
97
+ return (
98
+ <Box sx={{ ...FRAME, gap: "2px" }}>
99
+ {Array.from({ length: n }).map((_, column) => (
100
+ <Box key={column} sx={{ flex: 1, borderRadius: 0.5, bgcolor: barColor(active) }} />
101
+ ))}
102
+ </Box>
103
+ );
104
+ }
105
+
90
106
  /** Three columns of decreasing height — the board. */
91
107
  export function BoardGlyph({ active }: { active: boolean }): React.JSX.Element {
92
108
  return (
@@ -6,6 +6,7 @@ import { Box } from "../../../mui/Box";
6
6
  import {
7
7
  cardMinWidthForZoom,
8
8
  cardScaleForZoom,
9
+ DENSITY_BOARD_SCALE,
9
10
  DENSITY_CARD_COLUMNS,
10
11
  DENSITY_ROW_PADDING,
11
12
  useDataViewsLayout,
@@ -297,7 +298,10 @@ export function GridMain<T extends Record<string, unknown>>({
297
298
  renderCard={renderCard}
298
299
  selectedIds={c.selectedIds}
299
300
  onToggleId={c.toggleId}
300
- cardScale={cardScaleForZoom(zoom)}
301
+ // DENSITY, not zoom: the board's own knob in the Exibição tab is how
302
+ // wide its columns are, and it is the only sizing control the board
303
+ // has since the zoom slider was removed.
304
+ cardScale={DENSITY_BOARD_SCALE[density]}
301
305
  dataTestId={dataTestId}
302
306
  />
303
307
  </Headerless>
@@ -14,7 +14,7 @@ import {
14
14
  } from "./data-views-layout-context";
15
15
  import { FilterDialog, GridFilterPanel } from "./data-views-filter-panel";
16
16
  import { GridMain } from "./data-views-grid-bodies";
17
- import { InlineFilterChips, InlineFilterControls } from "./data-views-inline-bar";
17
+ import { InlineFilterControls } from "./data-views-inline-bar";
18
18
  import { toOverflowFields, useFilterOverflow, type OverflowSplit } from "./data-views-overflow";
19
19
  import { ShellToolbar } from "./data-views-shell-toolbar";
20
20
  import type { DisplayPanelView } from "./data-views-display-panel";
@@ -22,7 +22,8 @@ import type { DataViewExport } from "./data-views-export";
22
22
  import { DataViewsEmpty } from "./data-views-empty";
23
23
  import { DataViewsPagination } from "./data-views-pagination";
24
24
  import type { BoardConfig } from "./DataViewsBoard";
25
- import { DataViewsScopeTabs, type ScopeConfig } from "./data-views-scopes";
25
+ import { type ScopeConfig } from "./data-views-scopes";
26
+ import { ScopeTabsSlot } from "./data-views-scope-tabs";
26
27
  import { togglePillValues } from "./data-views-grid-helpers";
27
28
  import type {
28
29
  DataViewCardSelection,
@@ -233,8 +234,13 @@ interface GridShellProps<T extends Record<string, unknown>> {
233
234
  board?: BoardConfig<T>;
234
235
  /** Opt-in "Lista" layout: one full-width, entity-rendered row per record. */
235
236
  renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
236
- /** The page-level partition rendered as tabs above the toolbar. */
237
+ /** The page-level partition rendered as tabs under the toolbar. */
237
238
  scopes?: ScopeConfig[];
239
+ /**
240
+ * The row field the scopes partition by. Compared against the board's
241
+ * `groupBy` so the two are not shown as the same partition twice.
242
+ */
243
+ scopeFieldId?: string;
238
244
  /** Per-sort-field value kind, so directions read in the column's own terms. */
239
245
  sortKinds?: Record<string, string>;
240
246
  /** The saved-view chrome bracketing the Exibir panel. */
@@ -289,14 +295,6 @@ function ShellStack<T extends Record<string, unknown>>({
289
295
  <TableFilter open={c.filterOpen} onOpenChange={c.setFilterOpen} hasActiveFilters={c.activeFilterCount > 0}>
290
296
  <Stack spacing={0} data-testid={dataTestId ? `${dataTestId}-container` : undefined}>
291
297
  <GridHeaderRow title={props.title} headerActions={props.headerActions} testIdPrefix={testIdPrefix} />
292
- {/* Renders nothing (and reserves nothing) for an empty scope list. */}
293
- <DataViewsScopeTabs
294
- scopes={scopes}
295
- value={c.scope}
296
- onChange={c.setScope}
297
- counts={c.scopeCounts}
298
- testIdPrefix={testIdPrefix}
299
- />
300
298
  <ShellToolbar
301
299
  c={c}
302
300
  rows={props.rows}
@@ -308,6 +306,7 @@ function ShellStack<T extends Record<string, unknown>>({
308
306
  <InlineFilterControls
309
307
  {...filterProps}
310
308
  split={split}
309
+ activeFilterCount={c.activeFilterCount}
311
310
  onControlOpenChange={onControlOpenChange}
312
311
  />
313
312
  ) : undefined
@@ -318,11 +317,18 @@ function ShellStack<T extends Record<string, unknown>>({
318
317
  bulkActions={props.bulkActions}
319
318
  toolbarRightSlot={props.toolbarRightSlot}
320
319
  compactControls={showInline && split.compactControls}
320
+ counterHidden={showInline && split.counterHidden}
321
321
  showInline={showInline}
322
322
  />
323
- {/* Only the applied-filter chips live under the toolbar now — the
324
- controls themselves ride on the toolbar line above. */}
325
- {inlineVisible && <InlineFilterChips {...filterProps} />}
323
+ <ScopeTabsSlot
324
+ scopes={scopes}
325
+ scopeFieldId={props.scopeFieldId}
326
+ board={props.board}
327
+ value={c.scope}
328
+ onChange={c.setScope}
329
+ counts={c.scopeCounts}
330
+ testIdPrefix={testIdPrefix}
331
+ />
326
332
  <TableFilter.Layout>
327
333
  <TableFilter.Main>
328
334
  <GridMain