@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
|
@@ -2,180 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
|
|
4
4
|
import TuneRoundedIcon from "@mui/icons-material/TuneRounded";
|
|
5
|
-
import {
|
|
5
|
+
import { Popover } from "@mui/material";
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
|
|
8
|
-
import { MultiSelectDropdown } from "../../layout/ContentToolbar";
|
|
9
8
|
import { Button } from "../../form/Button";
|
|
10
9
|
import { Box } from "../../../mui/Box";
|
|
11
10
|
import { Text } from "../../typography/Text";
|
|
12
11
|
|
|
12
|
+
import { fieldClearing, MoreGroup, type MoreFieldProps } from "./data-views-more-fields";
|
|
13
13
|
import type { OverflowField } from "./data-views-overflow";
|
|
14
|
-
import { isRangeSet } from "./data-views-range-values";
|
|
15
|
-
import type { RangeValue } from "./data-views-types";
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
|
-
* "MAIS" — the filter overflow.
|
|
16
|
+
* "MAIS" — the filter overflow: its trigger, its panel and its footer. What
|
|
17
|
+
* goes INSIDE it is `data-views-more-fields`.
|
|
19
18
|
*
|
|
20
|
-
* It holds
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* a
|
|
25
|
-
*
|
|
26
|
-
* A field with one or two options renders FLAT — inside a panel there is no
|
|
27
|
-
* space to win back, and two labelled checkboxes are faster than a dropdown
|
|
28
|
-
* needing a second click. Past that it becomes a {@link MultiSelectDropdown}:
|
|
29
|
-
* see `INLINE_OPTION_LIMIT`.
|
|
19
|
+
* It holds the fields that had no room on the toolbar. An applied filter is
|
|
20
|
+
* ranked ahead of an idle one for the visible slots but is NOT exempt (see
|
|
21
|
+
* `useFilterOverflow`), so an applied filter can be in here — on a phone, most
|
|
22
|
+
* of them are. That is why the trigger changes tone and counts the applied
|
|
23
|
+
* ones: a filter you cannot see is a filter you forget you set, and the badge
|
|
24
|
+
* is what stops that.
|
|
30
25
|
*/
|
|
31
26
|
|
|
32
|
-
interface MoreFiltersProps<T extends Record<string, unknown>> {
|
|
27
|
+
interface MoreFiltersProps<T extends Record<string, unknown>> extends MoreFieldProps {
|
|
33
28
|
fields: OverflowField<T>[];
|
|
34
|
-
pills: Record<string, string[]>;
|
|
35
|
-
ranges: Record<string, RangeValue>;
|
|
36
|
-
onTogglePill: (fieldId: string, value: string, checked: boolean) => void;
|
|
37
|
-
onChangeRange: (fieldId: string, range: RangeValue) => void;
|
|
38
|
-
testIdPrefix: string;
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
32
|
+
* The trigger, badged with how many fields had no room on the bar — and told
|
|
33
|
+
* apart when some of them are APPLIED.
|
|
34
|
+
*
|
|
35
|
+
* An applied filter can land in here now (it is ranked first, not exempt), so
|
|
36
|
+
* this button is the only thing on screen still saying it exists. A neutral
|
|
37
|
+
* badge reading "3" would make "three filters you have not used" and "three
|
|
38
|
+
* filters narrowing this list" look identical, which is the whole failure the
|
|
39
|
+
* old exemption was written to avoid. So an overflow holding applied filters
|
|
40
|
+
* takes the applied tone and counts them in its own badge.
|
|
46
41
|
*/
|
|
47
|
-
const INLINE_OPTION_LIMIT = 2;
|
|
48
|
-
|
|
49
|
-
/** One overflowed pill: its options as checkboxes, all visible at once. */
|
|
50
|
-
function OverflowPill<T extends Record<string, unknown>>({
|
|
51
|
-
field,
|
|
52
|
-
values,
|
|
53
|
-
onToggle,
|
|
54
|
-
onClear,
|
|
55
|
-
testIdPrefix,
|
|
56
|
-
}: {
|
|
57
|
-
field: OverflowField<T>;
|
|
58
|
-
values: string[];
|
|
59
|
-
onToggle: (value: string, checked: boolean) => void;
|
|
60
|
-
onClear: () => void;
|
|
61
|
-
testIdPrefix: string;
|
|
62
|
-
}): React.JSX.Element {
|
|
63
|
-
const options = field.pill?.options ?? [];
|
|
64
|
-
if (options.length > INLINE_OPTION_LIMIT) {
|
|
65
|
-
return (
|
|
66
|
-
<MultiSelectDropdown
|
|
67
|
-
label={field.pill?.label ?? field.label}
|
|
68
|
-
options={options}
|
|
69
|
-
selected={new Set(values)}
|
|
70
|
-
onToggle={onToggle}
|
|
71
|
-
onClear={onClear}
|
|
72
|
-
allLabel="Todas"
|
|
73
|
-
searchable={options.length > 6 ? true : undefined}
|
|
74
|
-
searchPlaceholder="Buscar…"
|
|
75
|
-
noResultsLabel="Nenhum resultado"
|
|
76
|
-
layout="pill"
|
|
77
|
-
data-testid={`${testIdPrefix}-more-${field.id}`}
|
|
78
|
-
/>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
return (
|
|
82
|
-
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.75 }}>
|
|
83
|
-
{(field.pill?.options ?? []).map((option) => {
|
|
84
|
-
const checked = values.includes(option.value);
|
|
85
|
-
return (
|
|
86
|
-
<Box
|
|
87
|
-
key={option.value}
|
|
88
|
-
component="label"
|
|
89
|
-
data-testid={`${testIdPrefix}-more-${field.id}-${option.value}`}
|
|
90
|
-
sx={{
|
|
91
|
-
display: "inline-flex",
|
|
92
|
-
alignItems: "center",
|
|
93
|
-
gap: 0.25,
|
|
94
|
-
pr: 1,
|
|
95
|
-
border: 1,
|
|
96
|
-
borderStyle: "solid",
|
|
97
|
-
borderColor: checked ? "primary.main" : "divider",
|
|
98
|
-
bgcolor: checked ? "action.selected" : "transparent",
|
|
99
|
-
borderRadius: 5,
|
|
100
|
-
cursor: "pointer",
|
|
101
|
-
fontSize: "0.8125rem",
|
|
102
|
-
color: checked ? "primary.main" : "text.secondary",
|
|
103
|
-
}}
|
|
104
|
-
>
|
|
105
|
-
<Checkbox
|
|
106
|
-
size="small"
|
|
107
|
-
checked={checked}
|
|
108
|
-
onChange={(event) => onToggle(option.value, event.target.checked)}
|
|
109
|
-
inputProps={{ "aria-label": `${field.label}: ${option.label}` }}
|
|
110
|
-
/>
|
|
111
|
-
{option.label}
|
|
112
|
-
</Box>
|
|
113
|
-
);
|
|
114
|
-
})}
|
|
115
|
-
</Box>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** One overflowed range: its two bounds, as the same inputs the pill uses. */
|
|
120
|
-
function OverflowRange<T extends Record<string, unknown>>({
|
|
121
|
-
field,
|
|
122
|
-
value,
|
|
123
|
-
onChange,
|
|
124
|
-
testIdPrefix,
|
|
125
|
-
}: {
|
|
126
|
-
field: OverflowField<T>;
|
|
127
|
-
value: RangeValue;
|
|
128
|
-
onChange: (range: RangeValue) => void;
|
|
129
|
-
testIdPrefix: string;
|
|
130
|
-
}): React.JSX.Element {
|
|
131
|
-
const day = field.range?.kind === "day";
|
|
132
|
-
const set = (bound: "min" | "max", raw: string): void => {
|
|
133
|
-
const next: RangeValue = { ...value };
|
|
134
|
-
if (raw === "") delete next[bound];
|
|
135
|
-
else next[bound] = day ? raw : Number(raw);
|
|
136
|
-
onChange(next);
|
|
137
|
-
};
|
|
138
|
-
return (
|
|
139
|
-
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
140
|
-
{(["min", "max"] as const).map((bound, index) => (
|
|
141
|
-
<Box key={bound} sx={{ display: "contents" }}>
|
|
142
|
-
{index === 1 && <Box component="span" sx={{ color: "text.disabled" }}>–</Box>}
|
|
143
|
-
<Box
|
|
144
|
-
component="input"
|
|
145
|
-
type={day ? "date" : "number"}
|
|
146
|
-
placeholder={bound === "min" ? "de" : "até"}
|
|
147
|
-
aria-label={`${field.label} ${bound === "min" ? "de" : "até"}`}
|
|
148
|
-
data-testid={`${testIdPrefix}-more-${field.id}-${bound}`}
|
|
149
|
-
value={value[bound] ?? ""}
|
|
150
|
-
onChange={(event: React.ChangeEvent<HTMLInputElement>) => set(bound, event.target.value)}
|
|
151
|
-
sx={{
|
|
152
|
-
width: "100%",
|
|
153
|
-
px: 1,
|
|
154
|
-
py: 0.75,
|
|
155
|
-
border: 1,
|
|
156
|
-
borderStyle: "solid",
|
|
157
|
-
borderColor: "divider",
|
|
158
|
-
borderRadius: 1,
|
|
159
|
-
font: "inherit",
|
|
160
|
-
fontSize: "0.8125rem",
|
|
161
|
-
}}
|
|
162
|
-
/>
|
|
163
|
-
</Box>
|
|
164
|
-
))}
|
|
165
|
-
</Box>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** The trigger, badged with how many fields had no room on the bar. */
|
|
170
42
|
function MoreTrigger({
|
|
171
43
|
count,
|
|
44
|
+
appliedCount,
|
|
45
|
+
compact,
|
|
172
46
|
onOpen,
|
|
173
47
|
testIdPrefix,
|
|
174
48
|
}: {
|
|
175
49
|
count: number;
|
|
50
|
+
appliedCount: number;
|
|
51
|
+
/** Icon only — the same rung that strips Exibir/Exportar. */
|
|
52
|
+
compact: boolean;
|
|
176
53
|
onOpen: (anchor: HTMLElement) => void;
|
|
177
54
|
testIdPrefix: string;
|
|
178
55
|
}): React.JSX.Element {
|
|
56
|
+
const applied = appliedCount > 0;
|
|
179
57
|
return (
|
|
180
58
|
<Button
|
|
181
59
|
variant="outline"
|
|
@@ -183,64 +61,47 @@ function MoreTrigger({
|
|
|
183
61
|
color="neutral"
|
|
184
62
|
onClick={(event) => onOpen(event.currentTarget as HTMLElement)}
|
|
185
63
|
dataTestId={`${testIdPrefix}-more-filters`}
|
|
186
|
-
aria-label={
|
|
64
|
+
aria-label={
|
|
65
|
+
applied
|
|
66
|
+
? `Mais filtros: ${count} sem espaço na barra, ${appliedCount} aplicado(s)`
|
|
67
|
+
: `Mais filtros: ${count} sem espaço na barra`
|
|
68
|
+
}
|
|
187
69
|
>
|
|
188
|
-
<Box
|
|
70
|
+
<Box
|
|
71
|
+
component="span"
|
|
72
|
+
sx={{
|
|
73
|
+
display: "inline-flex",
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
gap: 0.5,
|
|
76
|
+
color: applied ? "primary.main" : undefined,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
189
79
|
<TuneRoundedIcon fontSize="small" />
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
80
|
+
{!compact && (
|
|
81
|
+
<Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
|
|
82
|
+
Mais
|
|
83
|
+
</Box>
|
|
84
|
+
)}
|
|
193
85
|
<Box
|
|
194
86
|
component="span"
|
|
195
|
-
|
|
87
|
+
data-testid={`${testIdPrefix}-more-badge`}
|
|
88
|
+
sx={{
|
|
89
|
+
px: 0.75,
|
|
90
|
+
borderRadius: 5,
|
|
91
|
+
bgcolor: applied ? "primary.main" : "action.selected",
|
|
92
|
+
color: applied ? "primary.contrastText" : undefined,
|
|
93
|
+
fontSize: "0.6875rem",
|
|
94
|
+
fontWeight: applied ? 700 : undefined,
|
|
95
|
+
}}
|
|
196
96
|
>
|
|
197
|
-
{count}
|
|
97
|
+
{applied ? appliedCount : count}
|
|
198
98
|
</Box>
|
|
199
|
-
<KeyboardArrowDownRoundedIcon fontSize="small" />
|
|
99
|
+
{!compact && <KeyboardArrowDownRoundedIcon fontSize="small" />}
|
|
200
100
|
</Box>
|
|
201
101
|
</Button>
|
|
202
102
|
);
|
|
203
103
|
}
|
|
204
104
|
|
|
205
|
-
/** One labelled group in the panel: the field's name, then its control. */
|
|
206
|
-
function MoreGroup<T extends Record<string, unknown>>({
|
|
207
|
-
field,
|
|
208
|
-
pills,
|
|
209
|
-
ranges,
|
|
210
|
-
onTogglePill,
|
|
211
|
-
onChangeRange,
|
|
212
|
-
testIdPrefix,
|
|
213
|
-
}: {
|
|
214
|
-
field: OverflowField<T>;
|
|
215
|
-
} & Omit<MoreFiltersProps<T>, "fields">): React.JSX.Element {
|
|
216
|
-
return (
|
|
217
|
-
<Box sx={{ mb: 1.5, "&:last-of-type": { mb: 0 } }}>
|
|
218
|
-
<Text variant="caption" as="p">
|
|
219
|
-
<Box component="span" sx={{ display: "block", mb: 0.5, color: "text.secondary" }}>
|
|
220
|
-
{field.label}
|
|
221
|
-
{field.group === "range" && isRangeSet(ranges[field.id] ?? {}) ? " •" : ""}
|
|
222
|
-
</Box>
|
|
223
|
-
</Text>
|
|
224
|
-
{field.group === "pill" ? (
|
|
225
|
-
<OverflowPill
|
|
226
|
-
field={field}
|
|
227
|
-
values={pills[field.id] ?? []}
|
|
228
|
-
onToggle={(value, checked) => onTogglePill(field.id, value, checked)}
|
|
229
|
-
onClear={() => (pills[field.id] ?? []).forEach((v) => onTogglePill(field.id, v, false))}
|
|
230
|
-
testIdPrefix={testIdPrefix}
|
|
231
|
-
/>
|
|
232
|
-
) : (
|
|
233
|
-
<OverflowRange
|
|
234
|
-
field={field}
|
|
235
|
-
value={ranges[field.id] ?? {}}
|
|
236
|
-
onChange={(range) => onChangeRange(field.id, range)}
|
|
237
|
-
testIdPrefix={testIdPrefix}
|
|
238
|
-
/>
|
|
239
|
-
)}
|
|
240
|
-
</Box>
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
105
|
/** The panel's heading: what these are, and why they are in here. */
|
|
245
106
|
function MoreHeading(): React.JSX.Element {
|
|
246
107
|
return (
|
|
@@ -269,30 +130,121 @@ function MoreHeading(): React.JSX.Element {
|
|
|
269
130
|
);
|
|
270
131
|
}
|
|
271
132
|
|
|
133
|
+
/**
|
|
134
|
+
* The panel's own way back to the unfiltered list.
|
|
135
|
+
*
|
|
136
|
+
* The bar's "Limpar" is the primary one, but it is a control like any other
|
|
137
|
+
* and the ladder can take its label — and on the narrowest rungs this panel is
|
|
138
|
+
* where most of the fields ended up anyway. Clearing everything from inside it
|
|
139
|
+
* saves closing it to reach a button three pixels wide.
|
|
140
|
+
*/
|
|
141
|
+
function MoreFooter({
|
|
142
|
+
onClearAll,
|
|
143
|
+
testIdPrefix,
|
|
144
|
+
}: {
|
|
145
|
+
onClearAll: () => void;
|
|
146
|
+
testIdPrefix: string;
|
|
147
|
+
}): React.JSX.Element {
|
|
148
|
+
return (
|
|
149
|
+
<Box sx={{ px: 1.5, py: 1, borderTop: 1, borderColor: "divider" }}>
|
|
150
|
+
<Box
|
|
151
|
+
component="button"
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={onClearAll}
|
|
154
|
+
data-testid={`${testIdPrefix}-more-clear-all`}
|
|
155
|
+
sx={{
|
|
156
|
+
width: "100%",
|
|
157
|
+
border: 0,
|
|
158
|
+
p: 0,
|
|
159
|
+
bgcolor: "transparent",
|
|
160
|
+
cursor: "pointer",
|
|
161
|
+
font: "inherit",
|
|
162
|
+
fontSize: "0.8125rem",
|
|
163
|
+
textAlign: "left",
|
|
164
|
+
color: "primary.main",
|
|
165
|
+
"&:hover": { textDecoration: "underline" },
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
Limpar todos os filtros
|
|
169
|
+
</Box>
|
|
170
|
+
</Box>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
272
174
|
/** The overflow trigger + panel. Renders nothing when everything fits. */
|
|
273
175
|
export function MoreFilters<T extends Record<string, unknown>>({
|
|
274
176
|
fields,
|
|
177
|
+
onOpenChange,
|
|
178
|
+
onClearAll,
|
|
179
|
+
anyApplied,
|
|
180
|
+
compact = false,
|
|
275
181
|
...rest
|
|
276
|
-
}: MoreFiltersProps<T>
|
|
182
|
+
}: MoreFiltersProps<T> & {
|
|
183
|
+
/** Icon only — the same rung that strips Exibir/Exportar. */
|
|
184
|
+
compact?: boolean;
|
|
185
|
+
/** Clears every filter, not just the ones in here. */
|
|
186
|
+
onClearAll: () => void;
|
|
187
|
+
/** Whether anything is applied anywhere — the footer is pointless otherwise. */
|
|
188
|
+
anyApplied: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* So the shell can FREEZE the measured split while this panel is open.
|
|
191
|
+
*
|
|
192
|
+
* Applying a filter in here makes its field active, and an active field
|
|
193
|
+
* takes an inline slot first — so the field being edited was promoted OUT of
|
|
194
|
+
* this panel mid-keystroke, taking the focused input with it. Typing "12"
|
|
195
|
+
* got as far as "1" and then had nowhere to go.
|
|
196
|
+
*/
|
|
197
|
+
onOpenChange?: (open: boolean) => void;
|
|
198
|
+
}): React.JSX.Element | null {
|
|
277
199
|
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
|
278
200
|
if (fields.length === 0) return null;
|
|
279
201
|
return (
|
|
280
202
|
<>
|
|
281
|
-
<MoreTrigger
|
|
203
|
+
<MoreTrigger
|
|
204
|
+
count={fields.length}
|
|
205
|
+
appliedCount={
|
|
206
|
+
fields.filter(
|
|
207
|
+
(field) =>
|
|
208
|
+
fieldClearing({
|
|
209
|
+
field,
|
|
210
|
+
pills: rest.pills,
|
|
211
|
+
ranges: rest.ranges,
|
|
212
|
+
onTogglePill: rest.onTogglePill,
|
|
213
|
+
onChangeRange: rest.onChangeRange,
|
|
214
|
+
}).applied,
|
|
215
|
+
).length
|
|
216
|
+
}
|
|
217
|
+
compact={compact}
|
|
218
|
+
onOpen={(element) => {
|
|
219
|
+
setAnchor(element);
|
|
220
|
+
onOpenChange?.(true);
|
|
221
|
+
}}
|
|
222
|
+
testIdPrefix={rest.testIdPrefix}
|
|
223
|
+
/>
|
|
282
224
|
<Popover
|
|
283
225
|
open={Boolean(anchor)}
|
|
284
226
|
anchorEl={anchor}
|
|
285
|
-
onClose={() =>
|
|
227
|
+
onClose={() => {
|
|
228
|
+
setAnchor(null);
|
|
229
|
+
onOpenChange?.(false);
|
|
230
|
+
}}
|
|
286
231
|
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
|
287
232
|
slotProps={{ paper: { sx: { width: 300, maxWidth: "calc(100vw - 32px)" } } }}
|
|
288
233
|
>
|
|
289
234
|
<Box data-testid={`${rest.testIdPrefix}-more-panel`}>
|
|
290
235
|
<MoreHeading />
|
|
291
|
-
|
|
236
|
+
{/* overflowX hidden, not auto: this panel is a fixed 300px and its
|
|
237
|
+
contents are told to shrink into it. A horizontal scrollbar here
|
|
238
|
+
means something refused to, which is a bug to fix rather than a
|
|
239
|
+
scrollbar to live with. */}
|
|
240
|
+
<Box sx={{ maxHeight: 320, overflowY: "auto", overflowX: "hidden", p: 1.5, minWidth: 0 }}>
|
|
292
241
|
{fields.map((field) => (
|
|
293
242
|
<MoreGroup key={field.id} field={field} {...rest} />
|
|
294
243
|
))}
|
|
295
244
|
</Box>
|
|
245
|
+
{anyApplied && (
|
|
246
|
+
<MoreFooter onClearAll={onClearAll} testIdPrefix={rest.testIdPrefix} />
|
|
247
|
+
)}
|
|
296
248
|
</Box>
|
|
297
249
|
</Popover>
|
|
298
250
|
</>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT EACH THING ON THE TOOLBAR COSTS, IN PIXELS.
|
|
3
|
+
*
|
|
4
|
+
* The pricing half of the collapse ladder, split from `data-views-overflow` at
|
|
5
|
+
* the file-size gate. Pure arithmetic and constants: no React, no measurement,
|
|
6
|
+
* no decisions. `data-views-overflow` measures the row and spends what this
|
|
7
|
+
* says it has.
|
|
8
|
+
*
|
|
9
|
+
* Every number here is either MEASURED in a browser (and says so) or
|
|
10
|
+
* deliberately generous. Over-pricing sheds one control too early, which the
|
|
11
|
+
* "Mais" button absorbs invisibly; under-pricing breaks the line.
|
|
12
|
+
*/
|
|
13
|
+
import type { OverflowField } from "./data-views-overflow";
|
|
14
|
+
import type { RangeValue } from "./data-views-types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Rough rendered width of a control, in px.
|
|
18
|
+
*
|
|
19
|
+
* An ESTIMATE from the label text rather than a real measurement, deliberately:
|
|
20
|
+
* measuring the true width needs every control mounted first, and mounting them
|
|
21
|
+
* to decide whether to mount them is the layout thrash this hook exists to
|
|
22
|
+
* avoid. The estimate only has to rank controls and find the cut point — being
|
|
23
|
+
* a few px out moves the cut by at most one control, and the overflow catches it.
|
|
24
|
+
*/
|
|
25
|
+
export function estimateWidth(text: string, extra: number): number {
|
|
26
|
+
/**
|
|
27
|
+
* ~7.6px per character at the toolbar's font size, plus 76px of chrome:
|
|
28
|
+
* horizontal padding, the border, and the dropdown chevron. The chrome used
|
|
29
|
+
* to be 52, which under-priced every control by roughly a chevron's width —
|
|
30
|
+
* six of them then "fitted" a row they overflowed, and because they are flex
|
|
31
|
+
* children the row paid for it by squeezing labels to "D…" and "V…" rather
|
|
32
|
+
* than by shedding a control into the overflow.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately generous: over-pricing sheds one control too early, which the
|
|
35
|
+
* "Mais" button absorbs invisibly. Under-pricing breaks the line.
|
|
36
|
+
*/
|
|
37
|
+
return Math.round(text.length * 7.6) + 76 + extra;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Is a range bounded at either end? */
|
|
41
|
+
export function isRangeSet(range: RangeValue | undefined): boolean {
|
|
42
|
+
return Boolean(range && (range.min !== undefined || range.max !== undefined));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The label a pill actually renders, which is what decides its width. */
|
|
46
|
+
export function pillText<T extends Record<string, unknown>>(field: OverflowField<T>, values: string[]): string {
|
|
47
|
+
if (values.length === 0) return field.label;
|
|
48
|
+
if (values.length === 1) {
|
|
49
|
+
const option = field.pill?.options.find((entry) => entry.value === values[0]);
|
|
50
|
+
return `${field.label}: ${option?.label ?? values[0]}`;
|
|
51
|
+
}
|
|
52
|
+
return `${field.label}: ${values.length}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Fixed toolbar furniture the filters have to fit around, in px. */
|
|
56
|
+
export const RESERVED = {
|
|
57
|
+
/** The search box never shrinks below this. */
|
|
58
|
+
search: 200,
|
|
59
|
+
/** The "N de N" counter. */
|
|
60
|
+
counter: 96,
|
|
61
|
+
/** Exibir + Exportar WITH their text labels. */
|
|
62
|
+
right: 216,
|
|
63
|
+
/**
|
|
64
|
+
* The same two controls as icons only — step 2 of the ladder.
|
|
65
|
+
*
|
|
66
|
+
* MEASURED at 133px for the pair (66 each: a Button's minimum tap target is
|
|
67
|
+
* a good deal wider than the 20px glyph inside it), so the old 96 under-priced
|
|
68
|
+
* them by a third and the row went on believing it had room it did not.
|
|
69
|
+
*/
|
|
70
|
+
rightCompact: 140,
|
|
71
|
+
/** What step 4 leaves behind: the search as a bare magnifier. */
|
|
72
|
+
searchIcon: 44,
|
|
73
|
+
/**
|
|
74
|
+
* The narrowest keyword box still worth typing into — below it the operator
|
|
75
|
+
* cannot read back what they wrote, and the box takes over the cluster
|
|
76
|
+
* instead of sharing it (see `searchTakeover`).
|
|
77
|
+
*
|
|
78
|
+
* 120 rather than 140 because this is compared against the ESTIMATE, which
|
|
79
|
+
* over-prices on purpose (see `estimateWidth`). At 140 a large phone took the
|
|
80
|
+
* whole cluster to produce a box barely wider than the one it could have had
|
|
81
|
+
* while keeping its filters — paying for nothing.
|
|
82
|
+
*/
|
|
83
|
+
usableSearch: 120,
|
|
84
|
+
/** The "Mais" button, only charged when there IS an overflow. */
|
|
85
|
+
overflowButton: 104,
|
|
86
|
+
/**
|
|
87
|
+
* "Limpar", only charged when there IS something to clear.
|
|
88
|
+
*
|
|
89
|
+
* It appears exactly when the row is at its widest — every applied filter
|
|
90
|
+
* has a longer label than the idle one it replaced — so leaving it unpriced
|
|
91
|
+
* would break the line at the one moment the operator most needs it.
|
|
92
|
+
* Measured at 64 as an icon; 72 with slack.
|
|
93
|
+
*/
|
|
94
|
+
clearAll: 72,
|
|
95
|
+
/** Gaps + the row's own padding. */
|
|
96
|
+
chrome: 64,
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
/** The gap between two controls. */
|
|
100
|
+
export const GAP = 8;
|