@12-apps/ui 1.22.0 → 2.0.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 +3 -1
- package/src/components/data-display/Alert/Alert.types.ts +2 -1
- package/src/components/data-display/Avatar/Avatar.types.ts +10 -2
- package/src/components/data-display/Badge/Badge.types.ts +3 -2
- package/src/components/data-display/Carousel/Carousel.parts.tsx +12 -10
- package/src/components/data-display/Carousel/Carousel.styles.ts +4 -3
- package/src/components/data-display/Carousel/Carousel.tsx +2 -1
- package/src/components/data-display/Carousel/Carousel.types.ts +7 -6
- package/src/components/data-display/Chart/Chart.tsx +2 -1
- package/src/components/data-display/Chart/Chart.types.ts +3 -2
- package/src/components/data-display/Chart/ChartSpec.types.ts +6 -7
- package/src/components/data-display/Chart/SpecChart.tsx +2 -1
- package/src/components/data-display/Chart/chart-internals.ts +3 -2
- package/src/components/data-display/Chart/chart-spec.ts +11 -1
- package/src/components/data-display/Chip/Chip.tsx +23 -3
- package/src/components/data-display/Chip/Chip.types.ts +37 -3
- package/src/components/data-display/Chip/chip.md +7 -3
- package/src/components/data-display/Chip/index.ts +1 -1
- package/src/components/data-display/DataViews/DataViewsGrid.tsx +10 -0
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +3 -0
- package/src/components/data-display/DataViews/RESPONSIVE-BEHAVIOUR.md +37 -3
- package/src/components/data-display/DataViews/data-views-day-input.tsx +22 -3
- package/src/components/data-display/DataViews/data-views-display-panel.tsx +4 -2
- package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +113 -62
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +17 -1
- package/src/components/data-display/DataViews/data-views-more-fields.tsx +16 -23
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +7 -2
- package/src/components/data-display/DataViews/data-views-number-input.tsx +14 -4
- package/src/components/data-display/DataViews/data-views-overflow-costs.ts +87 -16
- package/src/components/data-display/DataViews/data-views-overflow.ts +104 -71
- package/src/components/data-display/DataViews/index.ts +4 -0
- package/src/components/data-display/DataViews/list-card-rails.tsx +29 -0
- package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +8 -5
- package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.types.ts +2 -1
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.types.ts +2 -1
- package/src/components/data-display/LoadingState/LoadingState.types.ts +2 -1
- package/src/components/data-display/Progress/Progress.types.ts +3 -2
- package/src/components/data-display/Sheet/Sheet.body.tsx +2 -1
- package/src/components/data-display/Sheet/Sheet.styles.ts +3 -2
- package/src/components/data-display/Sheet/Sheet.types.ts +7 -2
- package/src/components/data-display/Stepper/Stepper.types.ts +2 -1
- package/src/components/data-display/Table/Table.styles.ts +1 -1
- package/src/components/data-display/Table/Table.types.ts +3 -1
- package/src/components/data-display/Tooltip/Tooltip.types.ts +2 -1
- package/src/components/feedback/Dialog/Dialog.types.ts +2 -1
- package/src/components/feedback/Modal/Modal.types.ts +2 -1
- package/src/components/form/Button/Button.types.ts +3 -2
- package/src/components/form/Command/Command.styles.ts +8 -7
- package/src/components/form/Command/Command.types.ts +3 -2
- package/src/components/form/CreatableSelect/CreatableSelect.types.ts +2 -1
- package/src/components/form/Input/Input.types.ts +2 -1
- package/src/components/form/InputOTP/InputOTP.types.ts +3 -2
- package/src/components/form/Label/Label.styles.ts +8 -5
- package/src/components/form/Label/Label.tsx +1 -1
- package/src/components/form/Label/Label.types.ts +3 -2
- package/src/components/form/Menubar/Menubar.styles.ts +10 -8
- package/src/components/form/Menubar/Menubar.tsx +2 -1
- package/src/components/form/Menubar/Menubar.types.ts +7 -6
- package/src/components/form/RadioGroup/RadioGroup.types.ts +3 -2
- package/src/components/form/Select/Select.tsx +13 -1
- package/src/components/form/Select/Select.types.ts +5 -2
- package/src/components/form/Slider/Slider.types.ts +3 -2
- package/src/components/form/Switch/Switch.types.ts +3 -2
- package/src/components/form/Textarea/Textarea.types.ts +3 -2
- package/src/components/form/Toggle/Toggle.types.ts +3 -2
- package/src/components/form/ToggleGroup/ToggleGroup.types.ts +3 -2
- package/src/components/layout/Separator/Separator.types.ts +2 -1
- package/src/components/layout/Spacer/Spacer.types.ts +2 -1
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +1 -1
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.types.ts +7 -2
- package/src/components/navigation/ContextMenu/ContextMenu.types.ts +3 -2
- package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +1 -0
- package/src/components/navigation/DropdownMenu/DropdownMenu.types.ts +12 -2
- package/src/components/navigation/NavigationMenu/NavigationMenu.types.ts +7 -2
- package/src/components/navigation/Pagination/Pagination.tsx +3 -1
- package/src/components/navigation/Pagination/Pagination.types.ts +5 -3
- package/src/components/navigation/Tabs/Tabs.types.ts +3 -2
- package/src/components/typography/Blockquote/Blockquote.types.ts +2 -1
- package/src/components/typography/Code/Code.types.ts +2 -1
- package/src/components/typography/Heading/Heading.types.ts +2 -1
- package/src/components/typography/Paragraph/Paragraph.types.ts +3 -2
- package/src/components/typography/Text/Text.types.ts +3 -2
- package/src/components/utility/AnimatedIcon/AnimatedIcon.types.ts +2 -1
- package/src/components/utility/Overflow/index.ts +6 -0
- package/src/components/utility/Overflow/overflow.ts +125 -0
- package/src/components/utility/WorkflowStep/WorkflowStep.types.ts +3 -2
- package/src/components/utility/index.ts +1 -0
- package/src/tokens/scales.ts +153 -0
|
@@ -107,8 +107,6 @@ export function DayBoundInput({
|
|
|
107
107
|
// The mask IS the placeholder, so the expected order is on screen before
|
|
108
108
|
// the first keystroke rather than discovered by getting it wrong.
|
|
109
109
|
placeholder="dd/mm/aaaa"
|
|
110
|
-
// A numeric keypad on a phone, while still accepting a paste with slashes.
|
|
111
|
-
inputMode="numeric"
|
|
112
110
|
value={text}
|
|
113
111
|
onChange={(event) => {
|
|
114
112
|
const next = maskBrDate(event.target.value);
|
|
@@ -126,7 +124,28 @@ export function DayBoundInput({
|
|
|
126
124
|
onBlur={() => setText(isoToBr(applied))}
|
|
127
125
|
// The placeholder occupies the space an un-shrunk label would render into.
|
|
128
126
|
InputLabelProps={{ shrink: true }}
|
|
129
|
-
|
|
127
|
+
// `inputMode` HAS to be here rather than on the TextField. Anything
|
|
128
|
+
// TextField does not recognise is spread onto its root `FormControl` div,
|
|
129
|
+
// and `inputMode` is not in that list — so passing it up there put the
|
|
130
|
+
// attribute on a non-editable wrapper, where it means nothing, and the
|
|
131
|
+
// phone went on offering the letter keyboard for a field that only takes
|
|
132
|
+
// digits (the `dd/mm/aaaa` hint sitting under a QWERTY layout). Only the
|
|
133
|
+
// `<input>` this reaches decides which keypad opens.
|
|
134
|
+
//
|
|
135
|
+
// `numeric` and not `tel`: both raise a keypad, but the phone one is a
|
|
136
|
+
// dialler with `+ * #` on it — keys that cannot occur in a date. A paste
|
|
137
|
+
// carrying separators still lands, because the MASK is what filters the
|
|
138
|
+
// text; the keypad only narrows what a thumb can reach for.
|
|
139
|
+
//
|
|
140
|
+
// Autofill off for the same reason: the browser's saved-value strip
|
|
141
|
+
// covers the keypad to suggest names and addresses, none of which are a
|
|
142
|
+
// date, and none of which the mask would keep.
|
|
143
|
+
inputProps={{
|
|
144
|
+
'data-testid': testId,
|
|
145
|
+
maxLength: 10,
|
|
146
|
+
inputMode: 'numeric',
|
|
147
|
+
autoComplete: 'off',
|
|
148
|
+
}}
|
|
130
149
|
sx={{ width: 165 }}
|
|
131
150
|
/>
|
|
132
151
|
);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
|
|
4
|
-
|
|
4
|
+
// Says "display settings" rather than the generic tune sliders it used to
|
|
5
|
+
// share with the "Mais" FILTER trigger — see the note there.
|
|
6
|
+
import DisplaySettingsRoundedIcon from "@mui/icons-material/DisplaySettingsRounded";
|
|
5
7
|
import { Popover } from "@mui/material";
|
|
6
8
|
import { useState } from "react";
|
|
7
9
|
|
|
@@ -267,7 +269,7 @@ function DisplayTrigger({
|
|
|
267
269
|
title={compact ? "Exibir" : undefined}
|
|
268
270
|
>
|
|
269
271
|
<Box component="span" sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}>
|
|
270
|
-
<
|
|
272
|
+
<DisplaySettingsRoundedIcon fontSize="small" />
|
|
271
273
|
{!compact && (
|
|
272
274
|
<Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
|
|
273
275
|
Exibir
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
3
5
|
import { DataGrid, type GridColumn, type GridSort } from "../DataGrid";
|
|
4
6
|
import { Box } from "../../../mui/Box";
|
|
5
7
|
|
|
@@ -10,9 +12,11 @@ import {
|
|
|
10
12
|
DENSITY_CARD_COLUMNS,
|
|
11
13
|
DENSITY_ROW_PADDING,
|
|
12
14
|
useDataViewsLayout,
|
|
15
|
+
type DataViewsDensity,
|
|
13
16
|
} from "./data-views-layout-context";
|
|
14
17
|
import { DataViewsBoard, type BoardConfig } from "./DataViewsBoard";
|
|
15
18
|
import { SelectAllStrip } from "./data-views-select-all-strip";
|
|
19
|
+
import { ListCardGroup, type ListGroupConfig } from "./list-card-rails";
|
|
16
20
|
import type { DataViewCardSelection } from "./data-views-types";
|
|
17
21
|
import type { DataViewsController } from "./use-data-views-state";
|
|
18
22
|
|
|
@@ -160,6 +164,10 @@ interface ListBodyProps<T extends Record<string, unknown>> {
|
|
|
160
164
|
onToggleId: (id: string | number) => void;
|
|
161
165
|
/** Gap between rows, from the density preference. */
|
|
162
166
|
rowGap: number;
|
|
167
|
+
/** The list's shared column config. See {@link ListGroupConfig}. */
|
|
168
|
+
group?: ListGroupConfig<T>;
|
|
169
|
+
/** The density every row answers, handed to the group when there is one. */
|
|
170
|
+
density: DataViewsDensity;
|
|
163
171
|
dataTestId?: string;
|
|
164
172
|
emptyState?: React.ReactNode;
|
|
165
173
|
}
|
|
@@ -186,28 +194,59 @@ function ListBody<T extends Record<string, unknown>>({
|
|
|
186
194
|
selectedIds,
|
|
187
195
|
onToggleId,
|
|
188
196
|
rowGap,
|
|
197
|
+
group,
|
|
198
|
+
density,
|
|
189
199
|
dataTestId,
|
|
190
200
|
emptyState,
|
|
191
201
|
}: ListBodyProps<T>): React.JSX.Element {
|
|
192
202
|
if (rows.length === 0) {
|
|
193
203
|
return <Box sx={{ mt: 1.5 }}>{emptyState}</Box>;
|
|
194
204
|
}
|
|
205
|
+
const testId = dataTestId ? `${dataTestId}-list` : "data-views-list";
|
|
206
|
+
const selectionFor = (id: string | number): DataViewCardSelection => ({
|
|
207
|
+
selected: selectedIds.has(id),
|
|
208
|
+
onToggleSelect: () => onToggleId(id),
|
|
209
|
+
scale: 1,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (group) {
|
|
213
|
+
return (
|
|
214
|
+
<Box sx={{ mt: 1.5 }}>
|
|
215
|
+
<ListCardGroup
|
|
216
|
+
cells={group.cells}
|
|
217
|
+
metaColumns={group.metaColumns}
|
|
218
|
+
rails={group.rails}
|
|
219
|
+
reserveGutters={group.reserveGutters}
|
|
220
|
+
density={density}
|
|
221
|
+
gap={rowGap}
|
|
222
|
+
dataTestId={testId}
|
|
223
|
+
>
|
|
224
|
+
{rows.map((row) => {
|
|
225
|
+
const id = getRowId(row);
|
|
226
|
+
// A FRAGMENT, NOT A BOX. The row is subgrid over the group's tracks
|
|
227
|
+
// (`gridColumn: span railCount`), which only resolves while the card
|
|
228
|
+
// is a DIRECT child of the group's grid. One wrapper element and the
|
|
229
|
+
// span is measured against a grid that isn't there, so every rail
|
|
230
|
+
// collapses — the exact failure the group exists to prevent.
|
|
231
|
+
return (
|
|
232
|
+
<React.Fragment key={id}>
|
|
233
|
+
{renderListRow(row, selectionFor(id))}
|
|
234
|
+
</React.Fragment>
|
|
235
|
+
);
|
|
236
|
+
})}
|
|
237
|
+
</ListCardGroup>
|
|
238
|
+
</Box>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
195
242
|
return (
|
|
196
243
|
<Box
|
|
197
244
|
sx={{ mt: 1.5, display: "flex", flexDirection: "column", gap: rowGap }}
|
|
198
|
-
data-testid={
|
|
245
|
+
data-testid={testId}
|
|
199
246
|
>
|
|
200
247
|
{rows.map((row) => {
|
|
201
248
|
const id = getRowId(row);
|
|
202
|
-
return (
|
|
203
|
-
<Box key={id}>
|
|
204
|
-
{renderListRow(row, {
|
|
205
|
-
selected: selectedIds.has(id),
|
|
206
|
-
onToggleSelect: () => onToggleId(id),
|
|
207
|
-
scale: 1,
|
|
208
|
-
})}
|
|
209
|
-
</Box>
|
|
210
|
-
);
|
|
249
|
+
return <Box key={id}>{renderListRow(row, selectionFor(id))}</Box>;
|
|
211
250
|
})}
|
|
212
251
|
</Box>
|
|
213
252
|
);
|
|
@@ -221,6 +260,8 @@ interface GridMainProps<T extends Record<string, unknown>> {
|
|
|
221
260
|
renderCard?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
222
261
|
/** Opt-in "Lista" layout — one full-width row per record. */
|
|
223
262
|
renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
263
|
+
/** The Lista's shared columns. Omitted, each row resolves its own tracks. */
|
|
264
|
+
listGroup?: ListGroupConfig<T>;
|
|
224
265
|
/** Opt-in "Quadro" (board) layout — needs `renderCard`, since it reuses the card. */
|
|
225
266
|
board?: BoardConfig<T>;
|
|
226
267
|
dataTestId?: string;
|
|
@@ -260,68 +301,78 @@ function Headerless<T extends Record<string, unknown>>({
|
|
|
260
301
|
);
|
|
261
302
|
}
|
|
262
303
|
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
emptyState
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const { layout, zoom, density } = useDataViewsLayout();
|
|
304
|
+
/**
|
|
305
|
+
* The body for one of the three HEADERLESS layouts, or `null` for the table.
|
|
306
|
+
*
|
|
307
|
+
* Split out from {@link GridMain} so the select-all strip is wrapped around the
|
|
308
|
+
* result exactly once instead of being repeated identically in every branch —
|
|
309
|
+
* which is also what keeps either function inside the line budget.
|
|
310
|
+
*/
|
|
311
|
+
function headerlessBody<T extends Record<string, unknown>>(
|
|
312
|
+
{ c, getRowId, renderCard, renderListRow, listGroup, board, dataTestId, emptyState }: GridMainProps<T>,
|
|
313
|
+
{ layout, zoom, density }: ReturnType<typeof useDataViewsLayout>,
|
|
314
|
+
): React.ReactNode | null {
|
|
275
315
|
if (layout === "list" && renderListRow) {
|
|
276
316
|
return (
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
<ListBody
|
|
318
|
+
rows={c.matched}
|
|
319
|
+
renderListRow={renderListRow}
|
|
320
|
+
getRowId={getRowId}
|
|
321
|
+
selectedIds={c.selectedIds}
|
|
322
|
+
onToggleId={c.toggleId}
|
|
323
|
+
rowGap={DENSITY_ROW_PADDING[density]}
|
|
324
|
+
group={listGroup}
|
|
325
|
+
density={density}
|
|
326
|
+
dataTestId={dataTestId}
|
|
327
|
+
emptyState={emptyState}
|
|
328
|
+
/>
|
|
289
329
|
);
|
|
290
330
|
}
|
|
291
331
|
if (layout === "board" && board && renderCard) {
|
|
292
332
|
return (
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
/>
|
|
307
|
-
</Headerless>
|
|
333
|
+
<DataViewsBoard
|
|
334
|
+
rows={c.matched}
|
|
335
|
+
board={board}
|
|
336
|
+
getRowId={getRowId}
|
|
337
|
+
renderCard={renderCard}
|
|
338
|
+
selectedIds={c.selectedIds}
|
|
339
|
+
onToggleId={c.toggleId}
|
|
340
|
+
// DENSITY, not zoom: the board's own knob in the Exibição tab is how
|
|
341
|
+
// wide its columns are, and it is the only sizing control the board
|
|
342
|
+
// has since the zoom slider was removed.
|
|
343
|
+
cardScale={DENSITY_BOARD_SCALE[density]}
|
|
344
|
+
dataTestId={dataTestId}
|
|
345
|
+
/>
|
|
308
346
|
);
|
|
309
347
|
}
|
|
310
348
|
if (layout === "cards" && renderCard) {
|
|
349
|
+
return (
|
|
350
|
+
<CardBody
|
|
351
|
+
rows={c.matched}
|
|
352
|
+
renderCard={renderCard}
|
|
353
|
+
getRowId={getRowId}
|
|
354
|
+
selectedIds={c.selectedIds}
|
|
355
|
+
onToggleId={c.toggleId}
|
|
356
|
+
minCardWidth={cardMinWidthForZoom(zoom)}
|
|
357
|
+
maxColumns={DENSITY_CARD_COLUMNS[density]}
|
|
358
|
+
cardScale={cardScaleForZoom(zoom)}
|
|
359
|
+
dataTestId={dataTestId}
|
|
360
|
+
emptyState={emptyState}
|
|
361
|
+
/>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Picks the body from the layout context: board, list, cards, or the dense grid. */
|
|
368
|
+
export function GridMain<T extends Record<string, unknown>>(props: GridMainProps<T>): React.JSX.Element {
|
|
369
|
+
const { c, getRowId, dataTestId, emptyState, testIdPrefix } = props;
|
|
370
|
+
const layoutState = useDataViewsLayout();
|
|
371
|
+
const body = headerlessBody(props, layoutState);
|
|
372
|
+
if (body) {
|
|
311
373
|
return (
|
|
312
374
|
<Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
|
|
313
|
-
|
|
314
|
-
rows={c.matched}
|
|
315
|
-
renderCard={renderCard}
|
|
316
|
-
getRowId={getRowId}
|
|
317
|
-
selectedIds={c.selectedIds}
|
|
318
|
-
onToggleId={c.toggleId}
|
|
319
|
-
minCardWidth={cardMinWidthForZoom(zoom)}
|
|
320
|
-
maxColumns={DENSITY_CARD_COLUMNS[density]}
|
|
321
|
-
cardScale={cardScaleForZoom(zoom)}
|
|
322
|
-
dataTestId={dataTestId}
|
|
323
|
-
emptyState={emptyState}
|
|
324
|
-
/>
|
|
375
|
+
{body}
|
|
325
376
|
</Headerless>
|
|
326
377
|
);
|
|
327
378
|
}
|
|
@@ -335,7 +386,7 @@ export function GridMain<T extends Record<string, unknown>>({
|
|
|
335
386
|
sortBy={c.state.sortBy}
|
|
336
387
|
onChangeSortBy={(next: GridSort[]) => c.patch({ sortBy: next })}
|
|
337
388
|
sortMode={c.serverMode ? "server" : "client"}
|
|
338
|
-
rowPadding={DENSITY_ROW_PADDING[density]}
|
|
389
|
+
rowPadding={DENSITY_ROW_PADDING[layoutState.density]}
|
|
339
390
|
dataTestId={dataTestId}
|
|
340
391
|
emptyState={emptyState}
|
|
341
392
|
/>
|
|
@@ -14,6 +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 type { ListGroupConfig } from "./list-card-rails";
|
|
17
18
|
import { InlineFilterControls } from "./data-views-inline-bar";
|
|
18
19
|
import { toOverflowFields, useFilterOverflow, type OverflowSplit } from "./data-views-overflow";
|
|
19
20
|
import { ShellToolbar } from "./data-views-shell-toolbar";
|
|
@@ -142,6 +143,8 @@ interface GridShellFiltersArgs<T extends Record<string, unknown>> {
|
|
|
142
143
|
rangeFields: RangeFieldConfig<T>[];
|
|
143
144
|
testIdPrefix: string;
|
|
144
145
|
alwaysShowSearch: boolean;
|
|
146
|
+
/** Is "Exportar" on the bar? Half the right cluster's cost when it is not. */
|
|
147
|
+
hasExport: boolean;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
function useGridShellFilters<T extends Record<string, unknown>>({
|
|
@@ -151,6 +154,7 @@ function useGridShellFilters<T extends Record<string, unknown>>({
|
|
|
151
154
|
rangeFields,
|
|
152
155
|
testIdPrefix,
|
|
153
156
|
alwaysShowSearch,
|
|
157
|
+
hasExport,
|
|
154
158
|
}: GridShellFiltersArgs<T>): {
|
|
155
159
|
showInline: boolean;
|
|
156
160
|
useModal: boolean;
|
|
@@ -171,6 +175,7 @@ function useGridShellFilters<T extends Record<string, unknown>>({
|
|
|
171
175
|
state.pills,
|
|
172
176
|
c.ranges,
|
|
173
177
|
openControls > 0,
|
|
178
|
+
hasExport,
|
|
174
179
|
);
|
|
175
180
|
// THE BAR RENDERS AT EVERY WIDTH. It used to be swapped for a full-screen
|
|
176
181
|
// filter MODAL below `lg`, which was the responsive strategy before the
|
|
@@ -234,6 +239,8 @@ interface GridShellProps<T extends Record<string, unknown>> {
|
|
|
234
239
|
board?: BoardConfig<T>;
|
|
235
240
|
/** Opt-in "Lista" layout: one full-width, entity-rendered row per record. */
|
|
236
241
|
renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
242
|
+
/** The Lista's shared columns, so its rows line up by construction. */
|
|
243
|
+
listGroup?: ListGroupConfig<T>;
|
|
237
244
|
/** The page-level partition rendered as tabs under the toolbar. */
|
|
238
245
|
scopes?: ScopeConfig[];
|
|
239
246
|
/**
|
|
@@ -336,6 +343,7 @@ function ShellStack<T extends Record<string, unknown>>({
|
|
|
336
343
|
getRowId={props.getRowId}
|
|
337
344
|
renderCard={props.renderCard}
|
|
338
345
|
renderListRow={props.renderListRow}
|
|
346
|
+
listGroup={props.listGroup}
|
|
339
347
|
board={props.board}
|
|
340
348
|
dataTestId={dataTestId}
|
|
341
349
|
testIdPrefix={testIdPrefix}
|
|
@@ -365,7 +373,15 @@ export function GridShell<T extends Record<string, unknown>>(props: GridShellPro
|
|
|
365
373
|
inlineFilters = false,
|
|
366
374
|
alwaysShowSearch = false,
|
|
367
375
|
} = props;
|
|
368
|
-
const filters = useGridShellFilters({
|
|
376
|
+
const filters = useGridShellFilters({
|
|
377
|
+
c,
|
|
378
|
+
inlineFilters,
|
|
379
|
+
fields,
|
|
380
|
+
rangeFields,
|
|
381
|
+
testIdPrefix,
|
|
382
|
+
alwaysShowSearch,
|
|
383
|
+
hasExport: props.exportConfig !== undefined,
|
|
384
|
+
});
|
|
369
385
|
return (
|
|
370
386
|
<DataViewsLayoutProvider
|
|
371
387
|
canUseCards={Boolean(renderCard)}
|
|
@@ -19,9 +19,7 @@ import { Box } from "../../../mui/Box";
|
|
|
19
19
|
import { Text } from "../../typography/Text";
|
|
20
20
|
|
|
21
21
|
import type { OverflowField } from "./data-views-overflow";
|
|
22
|
-
import { RangePresets } from "./data-views-preset-chips";
|
|
23
22
|
import { RangeBounds } from "./data-views-range-pill";
|
|
24
|
-
import { presetsFor } from "./data-views-range-presets";
|
|
25
23
|
import { isRangeSet } from "./data-views-range-values";
|
|
26
24
|
import type { RangeValue } from "./data-views-types";
|
|
27
25
|
|
|
@@ -114,10 +112,11 @@ function OverflowPill<T extends Record<string, unknown>>({
|
|
|
114
112
|
|
|
115
113
|
|
|
116
114
|
/**
|
|
117
|
-
* One overflowed range: its two bounds, as the same
|
|
115
|
+
* One overflowed range: its presets and its two bounds, as the same controls
|
|
116
|
+
* the pill uses.
|
|
118
117
|
*
|
|
119
|
-
* The
|
|
120
|
-
* which is the
|
|
118
|
+
* The whole control DELEGATES to {@link RangeBounds} rather than being built
|
|
119
|
+
* here, which is the point — this panel used to render a raw
|
|
121
120
|
* `<input type="date">`, so the masked `dd/mm/aaaa` field existed only while
|
|
122
121
|
* the filter FITTED on the bar. The moment "Data" overflowed it reverted to the
|
|
123
122
|
* native control the mask replaced, and a merchant on a narrow screen never saw
|
|
@@ -128,6 +127,12 @@ function OverflowPill<T extends Record<string, unknown>>({
|
|
|
128
127
|
* native date input reports a wide minimum (its own mask plus the picker
|
|
129
128
|
* glyph), which is what made two of them refuse to shrink and scrolled the
|
|
130
129
|
* panel sideways. A text input has no such minimum.
|
|
130
|
+
*
|
|
131
|
+
* `RangeBounds` renders the preset chips ITSELF, above the inputs. This panel
|
|
132
|
+
* must NOT render its own row as well: when the delegation above landed, the
|
|
133
|
+
* chips it used to draw by hand stayed behind, and "Data" showed Hoje/Ontem/
|
|
134
|
+
* Esta semana/Este mês/Este ano twice — two rows, duplicated test ids, and half
|
|
135
|
+
* the panel's height spent saying the same thing (FUT-751).
|
|
131
136
|
*/
|
|
132
137
|
function OverflowRange<T extends Record<string, unknown>>({
|
|
133
138
|
field,
|
|
@@ -144,24 +149,12 @@ function OverflowRange<T extends Record<string, unknown>>({
|
|
|
144
149
|
// set; the guard is for the type, not for a state the caller can reach.
|
|
145
150
|
if (!field.range) return null;
|
|
146
151
|
return (
|
|
147
|
-
<
|
|
148
|
-
{
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
presets={presetsFor(field.range)}
|
|
154
|
-
value={value}
|
|
155
|
-
onChange={onChange}
|
|
156
|
-
testId={`${testIdPrefix}-more-${field.id}`}
|
|
157
|
-
/>
|
|
158
|
-
<RangeBounds
|
|
159
|
-
field={field.range}
|
|
160
|
-
value={value}
|
|
161
|
-
onChange={onChange}
|
|
162
|
-
testId={`${testIdPrefix}-more-${field.id}`}
|
|
163
|
-
/>
|
|
164
|
-
</Box>
|
|
152
|
+
<RangeBounds
|
|
153
|
+
field={field.range}
|
|
154
|
+
value={value}
|
|
155
|
+
onChange={onChange}
|
|
156
|
+
testId={`${testIdPrefix}-more-${field.id}`}
|
|
157
|
+
/>
|
|
165
158
|
);
|
|
166
159
|
}
|
|
167
160
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
|
|
4
|
-
|
|
4
|
+
// A FUNNEL, not the tune sliders this used to share with "Exibir". The two
|
|
5
|
+
// sit a few px apart on the same bar and do unrelated things — one narrows
|
|
6
|
+
// WHICH rows you see, the other changes HOW they are shown — and with one
|
|
7
|
+
// glyph between them the bar offered the operator no way to tell which was
|
|
8
|
+
// which. On a phone, where both are icon-only, they were indistinguishable.
|
|
9
|
+
import FilterAltRoundedIcon from "@mui/icons-material/FilterAltRounded";
|
|
5
10
|
import { Popover } from "@mui/material";
|
|
6
11
|
import { useState } from "react";
|
|
7
12
|
|
|
@@ -76,7 +81,7 @@ function MoreTrigger({
|
|
|
76
81
|
color: applied ? "primary.main" : undefined,
|
|
77
82
|
}}
|
|
78
83
|
>
|
|
79
|
-
<
|
|
84
|
+
<FilterAltRoundedIcon fontSize="small" />
|
|
80
85
|
{!compact && (
|
|
81
86
|
<Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
|
|
82
87
|
Mais
|
|
@@ -23,6 +23,17 @@
|
|
|
23
23
|
* plain `50` still works, and so does `50.00` — a dot with exactly two digits
|
|
24
24
|
* after it is far more likely to be a decimal typed on a numeric keypad than a
|
|
25
25
|
* thousands group, and reading it as `5000` is the very failure this replaces.
|
|
26
|
+
*
|
|
27
|
+
* WHICH KEYPAD, and where the request has to be made. `inputMode` goes on the
|
|
28
|
+
* `<input>`, via `inputProps` — set on the `TextField` it rides the spread onto
|
|
29
|
+
* the root `FormControl` div, an element nothing types into, so the attribute
|
|
30
|
+
* has no effect and a phone goes on offering letters for a field that accepts
|
|
31
|
+
* none of them. It is `decimal` rather than `numeric` because the numeric pad
|
|
32
|
+
* has no comma on iOS, and the comma is the separator this field exists to
|
|
33
|
+
* accept — `numeric` here would reopen the bug above from the other side, with
|
|
34
|
+
* `17,50` untypeable rather than misread. Autofill is off alongside it: its
|
|
35
|
+
* saved-value strip covers the keypad to offer names and addresses, none of
|
|
36
|
+
* which survive `ALLOWED` anyway.
|
|
26
37
|
*/
|
|
27
38
|
import { Box, TextField } from '@mui/material';
|
|
28
39
|
import React, { useEffect, useState } from 'react';
|
|
@@ -107,9 +118,6 @@ export function NumberBoundInput({
|
|
|
107
118
|
size="small"
|
|
108
119
|
type="text"
|
|
109
120
|
label={label}
|
|
110
|
-
// Not `numeric`: that keypad has no comma on iOS, which is the separator
|
|
111
|
-
// this field exists to accept.
|
|
112
|
-
inputMode="decimal"
|
|
113
121
|
value={text}
|
|
114
122
|
onChange={(event) => {
|
|
115
123
|
// Reject the characters rather than silently dropping them AFTER
|
|
@@ -139,7 +147,9 @@ export function NumberBoundInput({
|
|
|
139
147
|
)
|
|
140
148
|
}
|
|
141
149
|
InputLabelProps={{ shrink: true }}
|
|
142
|
-
|
|
150
|
+
// `inputMode` belongs on the `<input>`, never on the TextField — see the
|
|
151
|
+
// keypad note at the top of this file for what goes wrong up there.
|
|
152
|
+
inputProps={{ 'data-testid': testId, inputMode: 'decimal', autoComplete: 'off' }}
|
|
143
153
|
InputProps={
|
|
144
154
|
unit
|
|
145
155
|
? {
|
|
@@ -33,8 +33,15 @@ export function estimateWidth(text: string, extra: number): number {
|
|
|
33
33
|
*
|
|
34
34
|
* Deliberately generous: over-pricing sheds one control too early, which the
|
|
35
35
|
* "Mais" button absorbs invisibly. Under-pricing breaks the line.
|
|
36
|
+
*
|
|
37
|
+
* The chrome was 76, and generous had turned into wrong. MEASURED on the
|
|
38
|
+
* Pedidos bar: "Pagamento" renders at 123px against an estimate of 144, and
|
|
39
|
+
* "Situação" at 107 against 137 — every pill over-priced by ~22%, which on a
|
|
40
|
+
* phone is the whole difference between one pill on the bar and none. 56 is
|
|
41
|
+
* still above the ~50 measured, so the slack the comment above asks for is
|
|
42
|
+
* intact; it is just slack rather than a second control's worth of budget.
|
|
36
43
|
*/
|
|
37
|
-
return Math.round(text.length * 7.6) +
|
|
44
|
+
return Math.round(text.length * 7.6) + 56 + extra;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/** Is a range bounded at either end? */
|
|
@@ -56,20 +63,40 @@ export function pillText<T extends Record<string, unknown>>(field: OverflowField
|
|
|
56
63
|
export const RESERVED = {
|
|
57
64
|
/** The search box never shrinks below this. */
|
|
58
65
|
search: 200,
|
|
59
|
-
/** The "N de N" counter. */
|
|
60
|
-
counter: 96,
|
|
61
|
-
/** Exibir + Exportar WITH their text labels. */
|
|
62
|
-
right: 216,
|
|
63
66
|
/**
|
|
64
|
-
* The
|
|
67
|
+
* The "N de N" counter.
|
|
68
|
+
*
|
|
69
|
+
* MEASURED at 35px, plus its divider and gap. The old 96 was priced for a
|
|
70
|
+
* counter reading "1.234 de 5.678"; a phone shows "3 de 3", and charging
|
|
71
|
+
* nearly triple its rendered width came straight out of the filter budget.
|
|
72
|
+
*/
|
|
73
|
+
counter: 56,
|
|
74
|
+
/**
|
|
75
|
+
* ONE right-hand control (Exibir, or Exportar) WITH its text label.
|
|
76
|
+
*
|
|
77
|
+
* Priced per control rather than for the pair, because the pair is not what
|
|
78
|
+
* every page renders: "Exportar" exists only when the host passes an
|
|
79
|
+
* `exportConfig`, and several pages (Pedidos among them) put their export in
|
|
80
|
+
* the PAGE HEADER instead. Charging 216 there billed the filters for a button
|
|
81
|
+
* that was never on the bar — ~108px of budget spent on nothing, which on a
|
|
82
|
+
* phone is the difference between one filter pill and none.
|
|
83
|
+
*/
|
|
84
|
+
displayControl: 108,
|
|
85
|
+
/**
|
|
86
|
+
* The same control as an icon only — step 2 of the ladder.
|
|
87
|
+
*
|
|
88
|
+
* MEASURED at 133px for the PAIR (66 each: a Button's minimum tap target is
|
|
89
|
+
* a good deal wider than the 20px glyph inside it); 70 keeps that total's
|
|
90
|
+
* slack at one control's granularity.
|
|
91
|
+
*/
|
|
92
|
+
displayControlCompact: 70,
|
|
93
|
+
/**
|
|
94
|
+
* What step 4 leaves behind: the search as a bare magnifier.
|
|
65
95
|
*
|
|
66
|
-
* MEASURED at
|
|
67
|
-
*
|
|
68
|
-
* them by a third and the row went on believing it had room it did not.
|
|
96
|
+
* MEASURED at 30px on the Pedidos bar (`…-search-all-collapsed`); 38 keeps a
|
|
97
|
+
* tap target's worth of slack over it.
|
|
69
98
|
*/
|
|
70
|
-
|
|
71
|
-
/** What step 4 leaves behind: the search as a bare magnifier. */
|
|
72
|
-
searchIcon: 44,
|
|
99
|
+
searchIcon: 38,
|
|
73
100
|
/**
|
|
74
101
|
* The narrowest keyword box still worth typing into — below it the operator
|
|
75
102
|
* cannot read back what they wrote, and the box takes over the cluster
|
|
@@ -81,8 +108,17 @@ export const RESERVED = {
|
|
|
81
108
|
* while keeping its filters — paying for nothing.
|
|
82
109
|
*/
|
|
83
110
|
usableSearch: 120,
|
|
84
|
-
/**
|
|
85
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The "Mais" button, only charged when there IS an overflow — and priced by
|
|
113
|
+
* the state it is actually in, like the right-hand cluster beside it.
|
|
114
|
+
*
|
|
115
|
+
* MEASURED: 128px with its label, badge and chevron; 69px once the ladder has
|
|
116
|
+
* it down to icon + badge. One number for both was wrong in BOTH directions —
|
|
117
|
+
* it under-priced the wide bar (the row's own failure mode) and over-priced
|
|
118
|
+
* the narrow one by a whole pill.
|
|
119
|
+
*/
|
|
120
|
+
overflowButton: 136,
|
|
121
|
+
overflowButtonCompact: 80,
|
|
86
122
|
/**
|
|
87
123
|
* "Limpar", only charged when there IS something to clear.
|
|
88
124
|
*
|
|
@@ -92,9 +128,44 @@ export const RESERVED = {
|
|
|
92
128
|
* Measured at 64 as an icon; 72 with slack.
|
|
93
129
|
*/
|
|
94
130
|
clearAll: 72,
|
|
95
|
-
/**
|
|
96
|
-
|
|
131
|
+
/**
|
|
132
|
+
* The row's own padding, plus the gaps BETWEEN the fixed furniture.
|
|
133
|
+
*
|
|
134
|
+
* Not the per-control gaps — `splitFilters` already adds a `GAP` for every
|
|
135
|
+
* filter it keeps, so counting a fourth one here charged one gap twice.
|
|
136
|
+
* MEASURED: 32px of padding (16 each side) + three 8px gaps between the
|
|
137
|
+
* search, the counter and the right cluster.
|
|
138
|
+
*/
|
|
139
|
+
chrome: 56,
|
|
97
140
|
} as const;
|
|
98
141
|
|
|
99
142
|
/** The gap between two controls. */
|
|
100
143
|
export const GAP = 8;
|
|
144
|
+
|
|
145
|
+
/** What the right-hand cluster costs, given what the host actually renders. */
|
|
146
|
+
export function rightClusterCost(hasExport: boolean, compact: boolean): number {
|
|
147
|
+
const one = compact ? RESERVED.displayControlCompact : RESERVED.displayControl;
|
|
148
|
+
return hasExport ? one * 2 : one;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* What the toolbar furniture costs at a given rung of the ladder — the budget
|
|
153
|
+
* the filter controls have to fit AROUND.
|
|
154
|
+
*
|
|
155
|
+
* This exists because the two halves used to disagree. `splitFilters` priced
|
|
156
|
+
* the furniture at its widest (search 200 + counter 96 + right 216) and shed
|
|
157
|
+
* every filter to make room; the ladder then collapsed those very controls to
|
|
158
|
+
* 44 + 0 + 140 and nobody handed the ~330px back. On a 428px phone that showed
|
|
159
|
+
* as a bar carrying a magnifier, a "Mais 5" pill, and a band of nothing.
|
|
160
|
+
*/
|
|
161
|
+
export function furnitureCost(
|
|
162
|
+
flags: { searchCollapsed: boolean; counterHidden: boolean; compactControls: boolean },
|
|
163
|
+
hasExport: boolean,
|
|
164
|
+
): number {
|
|
165
|
+
return (
|
|
166
|
+
(flags.searchCollapsed ? RESERVED.searchIcon : RESERVED.search) +
|
|
167
|
+
(flags.counterHidden ? 0 : RESERVED.counter) +
|
|
168
|
+
rightClusterCost(hasExport, flags.compactControls) +
|
|
169
|
+
RESERVED.chrome
|
|
170
|
+
);
|
|
171
|
+
}
|