@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.
- package/package.json +4 -4
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +1 -1
- package/src/components/data-display/DataViews/RESPONSIVE-BEHAVIOUR.md +183 -0
- package/src/components/data-display/DataViews/base-card.tsx +93 -23
- package/src/components/data-display/DataViews/base-list-card-slots.tsx +256 -0
- package/src/components/data-display/DataViews/base-list-card.tsx +352 -0
- package/src/components/data-display/DataViews/card-surface.ts +297 -0
- package/src/components/data-display/DataViews/data-views-bar-controls.tsx +119 -0
- package/src/components/data-display/DataViews/data-views-display-panel.tsx +48 -22
- package/src/components/data-display/DataViews/data-views-display-tabs.tsx +64 -31
- package/src/components/data-display/DataViews/data-views-drag.tsx +135 -0
- package/src/components/data-display/DataViews/data-views-export.tsx +2 -1
- package/src/components/data-display/DataViews/data-views-glyphs.tsx +16 -0
- package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +5 -1
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +20 -14
- package/src/components/data-display/DataViews/data-views-inline-bar.tsx +134 -155
- package/src/components/data-display/DataViews/data-views-layout-context.tsx +21 -0
- package/src/components/data-display/DataViews/data-views-more-fields.tsx +291 -0
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +151 -199
- package/src/components/data-display/DataViews/data-views-overflow-costs.ts +100 -0
- package/src/components/data-display/DataViews/data-views-overflow.ts +89 -73
- package/src/components/data-display/DataViews/data-views-preset-chips.tsx +100 -0
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +27 -11
- package/src/components/data-display/DataViews/data-views-range-presets.ts +121 -0
- package/src/components/data-display/DataViews/data-views-range-types.ts +94 -0
- package/src/components/data-display/DataViews/data-views-scope-tabs.tsx +57 -0
- package/src/components/data-display/DataViews/data-views-scopes.tsx +2 -2
- package/src/components/data-display/DataViews/data-views-search.tsx +19 -1
- package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +4 -0
- package/src/components/data-display/DataViews/data-views-toolbar.tsx +31 -21
- package/src/components/data-display/DataViews/data-views-types.ts +14 -47
- package/src/components/data-display/DataViews/index.ts +8 -0
- package/src/components/data-display/DataViews/list-card-rails.tsx +141 -0
- package/src/components/data-display/DataViews/pedidos.server.ts +118 -0
- package/src/components/data-display/DataViews/use-data-views-state.ts +1 -1
- package/src/components/data-display/DescriptionItem/DescriptionItem.tsx +26 -3
- package/src/components/layout/ContentToolbar/ContentToolbar.tsx +16 -1
- package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +9 -1
- 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
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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(
|
|
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:
|
|
226
|
-
{ value: "cozy", label:
|
|
227
|
-
{ value: "comfortable", label:
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|