@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,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
+