@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,291 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ONE FIELD, RENDERED INSIDE THE "MAIS" PANEL.
|
|
5
|
+
*
|
|
6
|
+
* Split from `data-views-more-filters` at the file-size gate, along the seam
|
|
7
|
+
* that was already there: that module is the trigger, the panel and its
|
|
8
|
+
* footer — the chrome — and this is what goes in it.
|
|
9
|
+
*
|
|
10
|
+
* A field with one or two options renders FLAT: inside a panel there is no
|
|
11
|
+
* space to win back, and two labelled checkboxes are faster than a dropdown
|
|
12
|
+
* needing a second click. Past that it becomes a {@link MultiSelectDropdown} —
|
|
13
|
+
* see `INLINE_OPTION_LIMIT`.
|
|
14
|
+
*/
|
|
15
|
+
import { Checkbox } from "@mui/material";
|
|
16
|
+
|
|
17
|
+
import { MultiSelectDropdown } from "../../layout/ContentToolbar";
|
|
18
|
+
import { Box } from "../../../mui/Box";
|
|
19
|
+
import { Text } from "../../typography/Text";
|
|
20
|
+
|
|
21
|
+
import type { OverflowField } from "./data-views-overflow";
|
|
22
|
+
import { RangePresets } from "./data-views-preset-chips";
|
|
23
|
+
import { presetsFor } from "./data-views-range-presets";
|
|
24
|
+
import { isRangeSet } from "./data-views-range-values";
|
|
25
|
+
import type { RangeValue } from "./data-views-types";
|
|
26
|
+
|
|
27
|
+
/** What each field's control needs, minus the panel's own chrome. */
|
|
28
|
+
export interface MoreFieldProps {
|
|
29
|
+
pills: Record<string, string[]>;
|
|
30
|
+
ranges: Record<string, RangeValue>;
|
|
31
|
+
onTogglePill: (fieldId: string, value: string, checked: boolean) => void;
|
|
32
|
+
onChangeRange: (fieldId: string, range: RangeValue) => void;
|
|
33
|
+
testIdPrefix: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Above this many options a field is a DROPDOWN rather than a flat row of
|
|
38
|
+
* checkboxes. Two options cost two lines and read faster laid out; a "Cliente"
|
|
39
|
+
* with a dozen turns the panel into a scrolling wall and buries every field
|
|
40
|
+
* under it, which is exactly what the overflow was meant to avoid.
|
|
41
|
+
*/
|
|
42
|
+
const INLINE_OPTION_LIMIT = 2;
|
|
43
|
+
|
|
44
|
+
/** One overflowed pill: its options as checkboxes, all visible at once. */
|
|
45
|
+
function OverflowPill<T extends Record<string, unknown>>({
|
|
46
|
+
field,
|
|
47
|
+
values,
|
|
48
|
+
onToggle,
|
|
49
|
+
onClear,
|
|
50
|
+
testIdPrefix,
|
|
51
|
+
}: {
|
|
52
|
+
field: OverflowField<T>;
|
|
53
|
+
values: string[];
|
|
54
|
+
onToggle: (value: string, checked: boolean) => void;
|
|
55
|
+
onClear: () => void;
|
|
56
|
+
testIdPrefix: string;
|
|
57
|
+
}): React.JSX.Element {
|
|
58
|
+
const options = field.pill?.options ?? [];
|
|
59
|
+
if (options.length > INLINE_OPTION_LIMIT) {
|
|
60
|
+
return (
|
|
61
|
+
<MultiSelectDropdown
|
|
62
|
+
label={field.pill?.label ?? field.label}
|
|
63
|
+
options={options}
|
|
64
|
+
selected={new Set(values)}
|
|
65
|
+
onToggle={onToggle}
|
|
66
|
+
onClear={onClear}
|
|
67
|
+
allLabel="Todas"
|
|
68
|
+
searchable={options.length > 6 ? true : undefined}
|
|
69
|
+
searchPlaceholder="Buscar…"
|
|
70
|
+
noResultsLabel="Nenhum resultado"
|
|
71
|
+
layout="pill"
|
|
72
|
+
data-testid={`${testIdPrefix}-more-${field.id}`}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return (
|
|
77
|
+
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.75 }}>
|
|
78
|
+
{(field.pill?.options ?? []).map((option) => {
|
|
79
|
+
const checked = values.includes(option.value);
|
|
80
|
+
return (
|
|
81
|
+
<Box
|
|
82
|
+
key={option.value}
|
|
83
|
+
component="label"
|
|
84
|
+
data-testid={`${testIdPrefix}-more-${field.id}-${option.value}`}
|
|
85
|
+
sx={{
|
|
86
|
+
display: "inline-flex",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
gap: 0.25,
|
|
89
|
+
pr: 1,
|
|
90
|
+
border: 1,
|
|
91
|
+
borderStyle: "solid",
|
|
92
|
+
borderColor: checked ? "primary.main" : "divider",
|
|
93
|
+
bgcolor: checked ? "action.selected" : "transparent",
|
|
94
|
+
borderRadius: 5,
|
|
95
|
+
cursor: "pointer",
|
|
96
|
+
fontSize: "0.8125rem",
|
|
97
|
+
color: checked ? "primary.main" : "text.secondary",
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<Checkbox
|
|
101
|
+
size="small"
|
|
102
|
+
checked={checked}
|
|
103
|
+
onChange={(event) => onToggle(option.value, event.target.checked)}
|
|
104
|
+
inputProps={{ "aria-label": `${field.label}: ${option.label}` }}
|
|
105
|
+
/>
|
|
106
|
+
{option.label}
|
|
107
|
+
</Box>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
/** One overflowed range: its two bounds, as the same inputs the pill uses. */
|
|
116
|
+
function OverflowRange<T extends Record<string, unknown>>({
|
|
117
|
+
field,
|
|
118
|
+
value,
|
|
119
|
+
onChange,
|
|
120
|
+
testIdPrefix,
|
|
121
|
+
}: {
|
|
122
|
+
field: OverflowField<T>;
|
|
123
|
+
value: RangeValue;
|
|
124
|
+
onChange: (range: RangeValue) => void;
|
|
125
|
+
testIdPrefix: string;
|
|
126
|
+
}): React.JSX.Element {
|
|
127
|
+
const day = field.range?.kind === "day";
|
|
128
|
+
const set = (bound: "min" | "max", raw: string): void => {
|
|
129
|
+
const next: RangeValue = { ...value };
|
|
130
|
+
if (raw === "") delete next[bound];
|
|
131
|
+
else next[bound] = day ? raw : Number(raw);
|
|
132
|
+
onChange(next);
|
|
133
|
+
};
|
|
134
|
+
return (
|
|
135
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
136
|
+
{/* The overflow offers the same one-click windows the pill does. A field
|
|
137
|
+
in here is one that had no room on the bar, not one with fewer
|
|
138
|
+
controls — otherwise "Data" gains and loses its presets as the window
|
|
139
|
+
is resized. */}
|
|
140
|
+
<RangePresets
|
|
141
|
+
presets={field.range ? presetsFor(field.range) : []}
|
|
142
|
+
value={value}
|
|
143
|
+
onChange={onChange}
|
|
144
|
+
testId={`${testIdPrefix}-more-${field.id}`}
|
|
145
|
+
/>
|
|
146
|
+
<Box sx={{ display: "flex", alignItems: "center", gap: 1, minWidth: 0 }}>
|
|
147
|
+
{(["min", "max"] as const).map((bound, index) => (
|
|
148
|
+
<Box key={bound} sx={{ display: "contents" }}>
|
|
149
|
+
{index === 1 && <Box component="span" sx={{ color: "text.disabled" }}>–</Box>}
|
|
150
|
+
<Box
|
|
151
|
+
component="input"
|
|
152
|
+
type={day ? "date" : "number"}
|
|
153
|
+
placeholder={bound === "min" ? "de" : "até"}
|
|
154
|
+
aria-label={`${field.label} ${bound === "min" ? "de" : "até"}`}
|
|
155
|
+
data-testid={`${testIdPrefix}-more-${field.id}-${bound}`}
|
|
156
|
+
value={value[bound] ?? ""}
|
|
157
|
+
onChange={(event: React.ChangeEvent<HTMLInputElement>) => set(bound, event.target.value)}
|
|
158
|
+
sx={{
|
|
159
|
+
// `flex:1` + `minWidth:0` rather than `width:100%`. A native
|
|
160
|
+
// date input reports a wide intrinsic minimum (its dd/mm/aaaa
|
|
161
|
+
// mask plus the picker glyph), and a flex child defaults to
|
|
162
|
+
// min-width:auto — so two of them plus the dash refused to
|
|
163
|
+
// shrink and the whole panel scrolled sideways.
|
|
164
|
+
flex: "1 1 0",
|
|
165
|
+
minWidth: 0,
|
|
166
|
+
px: 1,
|
|
167
|
+
py: 0.75,
|
|
168
|
+
border: 1,
|
|
169
|
+
borderStyle: "solid",
|
|
170
|
+
borderColor: "divider",
|
|
171
|
+
borderRadius: 1,
|
|
172
|
+
font: "inherit",
|
|
173
|
+
fontSize: "0.8125rem",
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
</Box>
|
|
177
|
+
))}
|
|
178
|
+
</Box>
|
|
179
|
+
</Box>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The trigger, badged with how many fields had no room on the bar — and told
|
|
185
|
+
* apart when some of them are APPLIED.
|
|
186
|
+
*
|
|
187
|
+
* An applied filter can land in here now (it is ranked first, not exempt), so
|
|
188
|
+
* this button is the only thing on screen still saying it exists. A neutral
|
|
189
|
+
* badge reading "3" would make "three filters you have not used" and "three
|
|
190
|
+
* filters narrowing this list" look identical, which is the whole failure the
|
|
191
|
+
* old exemption was written to avoid. So an overflow holding applied filters
|
|
192
|
+
* takes the applied tone and counts them in its own badge.
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Whether a field has anything applied, and how to unapply it — the two shapes
|
|
196
|
+
* (a pill's selected values, a range's bounds) answered in one place so the
|
|
197
|
+
* group that renders them stays a renderer.
|
|
198
|
+
*/
|
|
199
|
+
export function fieldClearing<T extends Record<string, unknown>>({
|
|
200
|
+
field,
|
|
201
|
+
pills,
|
|
202
|
+
ranges,
|
|
203
|
+
onTogglePill,
|
|
204
|
+
onChangeRange,
|
|
205
|
+
}: {
|
|
206
|
+
field: OverflowField<T>;
|
|
207
|
+
} & Pick<MoreFieldProps, "pills" | "ranges" | "onTogglePill" | "onChangeRange">): {
|
|
208
|
+
applied: boolean;
|
|
209
|
+
clear: () => void;
|
|
210
|
+
} {
|
|
211
|
+
if (field.group === "range") {
|
|
212
|
+
return {
|
|
213
|
+
applied: isRangeSet(ranges[field.id] ?? {}),
|
|
214
|
+
clear: () => onChangeRange(field.id, {}),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const values = pills[field.id] ?? [];
|
|
218
|
+
return {
|
|
219
|
+
applied: values.length > 0,
|
|
220
|
+
clear: () => values.forEach((value) => onTogglePill(field.id, value, false)),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
/** One labelled group in the panel: the field's name, then its control. */
|
|
226
|
+
export function MoreGroup<T extends Record<string, unknown>>({
|
|
227
|
+
field,
|
|
228
|
+
pills,
|
|
229
|
+
ranges,
|
|
230
|
+
onTogglePill,
|
|
231
|
+
onChangeRange,
|
|
232
|
+
testIdPrefix,
|
|
233
|
+
}: {
|
|
234
|
+
field: OverflowField<T>;
|
|
235
|
+
} & MoreFieldProps): React.JSX.Element {
|
|
236
|
+
const { applied, clear } = fieldClearing({ field, pills, ranges, onTogglePill, onChangeRange });
|
|
237
|
+
return (
|
|
238
|
+
<Box sx={{ mb: 1.5, "&:last-of-type": { mb: 0 } }}>
|
|
239
|
+
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 0.5 }}>
|
|
240
|
+
<Text variant="caption" as="span">
|
|
241
|
+
<Box component="span" sx={{ color: "text.secondary" }}>
|
|
242
|
+
{field.label}
|
|
243
|
+
{field.group === "range" && isRangeSet(ranges[field.id] ?? {}) ? " •" : ""}
|
|
244
|
+
</Box>
|
|
245
|
+
</Text>
|
|
246
|
+
{/* The panel's equivalent of the pill's ✕. Without it a field applied
|
|
247
|
+
in here could only be cleared by finding it again on the bar —
|
|
248
|
+
which is where it goes the moment it becomes active, so the
|
|
249
|
+
operator has to close this panel to undo what they just did. */}
|
|
250
|
+
{applied && (
|
|
251
|
+
<Box
|
|
252
|
+
component="button"
|
|
253
|
+
type="button"
|
|
254
|
+
onClick={clear}
|
|
255
|
+
data-testid={`${testIdPrefix}-more-${field.id}-clear`}
|
|
256
|
+
sx={{
|
|
257
|
+
ml: "auto",
|
|
258
|
+
border: 0,
|
|
259
|
+
p: 0,
|
|
260
|
+
bgcolor: "transparent",
|
|
261
|
+
cursor: "pointer",
|
|
262
|
+
font: "inherit",
|
|
263
|
+
fontSize: "0.75rem",
|
|
264
|
+
color: "primary.main",
|
|
265
|
+
"&:hover": { textDecoration: "underline" },
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
Limpar
|
|
269
|
+
</Box>
|
|
270
|
+
)}
|
|
271
|
+
</Box>
|
|
272
|
+
{field.group === "pill" ? (
|
|
273
|
+
<OverflowPill
|
|
274
|
+
field={field}
|
|
275
|
+
values={pills[field.id] ?? []}
|
|
276
|
+
onToggle={(value, checked) => onTogglePill(field.id, value, checked)}
|
|
277
|
+
onClear={() => (pills[field.id] ?? []).forEach((v) => onTogglePill(field.id, v, false))}
|
|
278
|
+
testIdPrefix={testIdPrefix}
|
|
279
|
+
/>
|
|
280
|
+
) : (
|
|
281
|
+
<OverflowRange
|
|
282
|
+
field={field}
|
|
283
|
+
value={ranges[field.id] ?? {}}
|
|
284
|
+
onChange={(range) => onChangeRange(field.id, range)}
|
|
285
|
+
testIdPrefix={testIdPrefix}
|
|
286
|
+
/>
|
|
287
|
+
)}
|
|
288
|
+
</Box>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|