@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.
Files changed (88) hide show
  1. package/package.json +3 -1
  2. package/src/components/data-display/Alert/Alert.types.ts +2 -1
  3. package/src/components/data-display/Avatar/Avatar.types.ts +10 -2
  4. package/src/components/data-display/Badge/Badge.types.ts +3 -2
  5. package/src/components/data-display/Carousel/Carousel.parts.tsx +12 -10
  6. package/src/components/data-display/Carousel/Carousel.styles.ts +4 -3
  7. package/src/components/data-display/Carousel/Carousel.tsx +2 -1
  8. package/src/components/data-display/Carousel/Carousel.types.ts +7 -6
  9. package/src/components/data-display/Chart/Chart.tsx +2 -1
  10. package/src/components/data-display/Chart/Chart.types.ts +3 -2
  11. package/src/components/data-display/Chart/ChartSpec.types.ts +6 -7
  12. package/src/components/data-display/Chart/SpecChart.tsx +2 -1
  13. package/src/components/data-display/Chart/chart-internals.ts +3 -2
  14. package/src/components/data-display/Chart/chart-spec.ts +11 -1
  15. package/src/components/data-display/Chip/Chip.tsx +23 -3
  16. package/src/components/data-display/Chip/Chip.types.ts +37 -3
  17. package/src/components/data-display/Chip/chip.md +7 -3
  18. package/src/components/data-display/Chip/index.ts +1 -1
  19. package/src/components/data-display/DataViews/DataViewsGrid.tsx +10 -0
  20. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +3 -0
  21. package/src/components/data-display/DataViews/RESPONSIVE-BEHAVIOUR.md +37 -3
  22. package/src/components/data-display/DataViews/data-views-day-input.tsx +22 -3
  23. package/src/components/data-display/DataViews/data-views-display-panel.tsx +4 -2
  24. package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +113 -62
  25. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +17 -1
  26. package/src/components/data-display/DataViews/data-views-more-fields.tsx +16 -23
  27. package/src/components/data-display/DataViews/data-views-more-filters.tsx +7 -2
  28. package/src/components/data-display/DataViews/data-views-number-input.tsx +14 -4
  29. package/src/components/data-display/DataViews/data-views-overflow-costs.ts +87 -16
  30. package/src/components/data-display/DataViews/data-views-overflow.ts +104 -71
  31. package/src/components/data-display/DataViews/index.ts +4 -0
  32. package/src/components/data-display/DataViews/list-card-rails.tsx +29 -0
  33. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +8 -5
  34. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.types.ts +2 -1
  35. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.types.ts +2 -1
  36. package/src/components/data-display/LoadingState/LoadingState.types.ts +2 -1
  37. package/src/components/data-display/Progress/Progress.types.ts +3 -2
  38. package/src/components/data-display/Sheet/Sheet.body.tsx +2 -1
  39. package/src/components/data-display/Sheet/Sheet.styles.ts +3 -2
  40. package/src/components/data-display/Sheet/Sheet.types.ts +7 -2
  41. package/src/components/data-display/Stepper/Stepper.types.ts +2 -1
  42. package/src/components/data-display/Table/Table.styles.ts +1 -1
  43. package/src/components/data-display/Table/Table.types.ts +3 -1
  44. package/src/components/data-display/Tooltip/Tooltip.types.ts +2 -1
  45. package/src/components/feedback/Dialog/Dialog.types.ts +2 -1
  46. package/src/components/feedback/Modal/Modal.types.ts +2 -1
  47. package/src/components/form/Button/Button.types.ts +3 -2
  48. package/src/components/form/Command/Command.styles.ts +8 -7
  49. package/src/components/form/Command/Command.types.ts +3 -2
  50. package/src/components/form/CreatableSelect/CreatableSelect.types.ts +2 -1
  51. package/src/components/form/Input/Input.types.ts +2 -1
  52. package/src/components/form/InputOTP/InputOTP.types.ts +3 -2
  53. package/src/components/form/Label/Label.styles.ts +8 -5
  54. package/src/components/form/Label/Label.tsx +1 -1
  55. package/src/components/form/Label/Label.types.ts +3 -2
  56. package/src/components/form/Menubar/Menubar.styles.ts +10 -8
  57. package/src/components/form/Menubar/Menubar.tsx +2 -1
  58. package/src/components/form/Menubar/Menubar.types.ts +7 -6
  59. package/src/components/form/RadioGroup/RadioGroup.types.ts +3 -2
  60. package/src/components/form/Select/Select.tsx +13 -1
  61. package/src/components/form/Select/Select.types.ts +5 -2
  62. package/src/components/form/Slider/Slider.types.ts +3 -2
  63. package/src/components/form/Switch/Switch.types.ts +3 -2
  64. package/src/components/form/Textarea/Textarea.types.ts +3 -2
  65. package/src/components/form/Toggle/Toggle.types.ts +3 -2
  66. package/src/components/form/ToggleGroup/ToggleGroup.types.ts +3 -2
  67. package/src/components/layout/Separator/Separator.types.ts +2 -1
  68. package/src/components/layout/Spacer/Spacer.types.ts +2 -1
  69. package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +1 -1
  70. package/src/components/navigation/Breadcrumbs/Breadcrumbs.types.ts +7 -2
  71. package/src/components/navigation/ContextMenu/ContextMenu.types.ts +3 -2
  72. package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +1 -0
  73. package/src/components/navigation/DropdownMenu/DropdownMenu.types.ts +12 -2
  74. package/src/components/navigation/NavigationMenu/NavigationMenu.types.ts +7 -2
  75. package/src/components/navigation/Pagination/Pagination.tsx +3 -1
  76. package/src/components/navigation/Pagination/Pagination.types.ts +5 -3
  77. package/src/components/navigation/Tabs/Tabs.types.ts +3 -2
  78. package/src/components/typography/Blockquote/Blockquote.types.ts +2 -1
  79. package/src/components/typography/Code/Code.types.ts +2 -1
  80. package/src/components/typography/Heading/Heading.types.ts +2 -1
  81. package/src/components/typography/Paragraph/Paragraph.types.ts +3 -2
  82. package/src/components/typography/Text/Text.types.ts +3 -2
  83. package/src/components/utility/AnimatedIcon/AnimatedIcon.types.ts +2 -1
  84. package/src/components/utility/Overflow/index.ts +6 -0
  85. package/src/components/utility/Overflow/overflow.ts +125 -0
  86. package/src/components/utility/WorkflowStep/WorkflowStep.types.ts +3 -2
  87. package/src/components/utility/index.ts +1 -0
  88. 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
- inputProps={{ 'data-testid': testId, maxLength: 10 }}
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
- import TuneRoundedIcon from "@mui/icons-material/TuneRounded";
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
- <TuneRoundedIcon fontSize="small" />
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={dataTestId ? `${dataTestId}-list` : "data-views-list"}
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
- /** Picks the body from the layout context: board, list, cards, or the dense grid. */
264
- export function GridMain<T extends Record<string, unknown>>({
265
- c,
266
- getRowId,
267
- renderCard,
268
- renderListRow,
269
- board,
270
- dataTestId,
271
- emptyState,
272
- testIdPrefix,
273
- }: GridMainProps<T>): React.JSX.Element {
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
- <Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
278
- <ListBody
279
- rows={c.matched}
280
- renderListRow={renderListRow}
281
- getRowId={getRowId}
282
- selectedIds={c.selectedIds}
283
- onToggleId={c.toggleId}
284
- rowGap={DENSITY_ROW_PADDING[density]}
285
- dataTestId={dataTestId}
286
- emptyState={emptyState}
287
- />
288
- </Headerless>
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
- <Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
294
- <DataViewsBoard
295
- rows={c.matched}
296
- board={board}
297
- getRowId={getRowId}
298
- renderCard={renderCard}
299
- selectedIds={c.selectedIds}
300
- onToggleId={c.toggleId}
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]}
305
- dataTestId={dataTestId}
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
- <CardBody
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({ c, inlineFilters, fields, rangeFields, testIdPrefix, alwaysShowSearch });
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 inputs the pill uses.
115
+ * One overflowed range: its presets and its two bounds, as the same controls
116
+ * the pill uses.
118
117
  *
119
- * The bounds DELEGATE to {@link RangeBounds} rather than being built here,
120
- * which is the whole point — this panel used to render a raw
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
- <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
148
- {/* The overflow offers the same one-click windows the pill does. A field
149
- in here is one that had no room on the bar, not one with fewer
150
- controls — otherwise "Data" gains and loses its presets as the window
151
- is resized. */}
152
- <RangePresets
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
- import TuneRoundedIcon from "@mui/icons-material/TuneRounded";
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
- <TuneRoundedIcon fontSize="small" />
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
- inputProps={{ 'data-testid': testId }}
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) + 76 + extra;
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 same two controls as icons only — step 2 of the ladder.
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 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.
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
- rightCompact: 140,
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
- /** The "Mais" button, only charged when there IS an overflow. */
85
- overflowButton: 104,
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
- /** Gaps + the row's own padding. */
96
- chrome: 64,
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
+ }