@12-apps/ui 1.22.0 → 1.23.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 +1 -1
- 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-parts.tsx +13 -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 +87 -26
package/package.json
CHANGED
|
@@ -23,6 +23,10 @@ later; an eight-filter one degrades sooner. Both are correct.
|
|
|
23
23
|
| small desktop | 1280 | laptop |
|
|
24
24
|
| large desktop | 1600 | external monitor |
|
|
25
25
|
|
|
26
|
+
The tests also pin a **crowded** row at 1024, because a small desktop no longer
|
|
27
|
+
overflows at all: the rules about what happens when controls do NOT fit need a
|
|
28
|
+
width where some of them do not.
|
|
29
|
+
|
|
26
30
|
**These are toolbar ROW widths, not viewport widths.** The `ResizeObserver`
|
|
27
31
|
watches the row, and the row is narrower than the window by whatever the page
|
|
28
32
|
puts around it — around 48px in the Pedidos shell. So a 430px row is roughly a
|
|
@@ -77,8 +81,26 @@ Read this as "what the Pedidos table does", not "what the code hard-codes".
|
|
|
77
81
|
|
|
78
82
|
| | small mobile | large mobile | tablet | small desktop | large desktop |
|
|
79
83
|
| --- | --- | --- | --- | --- | --- |
|
|
80
|
-
| controls on the bar | 0 |
|
|
81
|
-
| "Mais" present | yes | yes | yes |
|
|
84
|
+
| controls on the bar | 0 | 1 | 1 | all 6 | all 6 |
|
|
85
|
+
| "Mais" present | yes | yes | yes | no | no |
|
|
86
|
+
|
|
87
|
+
**The bar re-spends what its own collapse frees.** Rungs 2/4/5 turn 200 + 96 +
|
|
88
|
+
216 of furniture into 44 + 0 + 140, and the split used to be decided once,
|
|
89
|
+
against the UNCOLLAPSED row — so a phone shed all six controls to make room for
|
|
90
|
+
a search box and a counter that were about to shrink, and nobody handed the
|
|
91
|
+
~330px back. It showed as a 428px bar carrying a magnifier, "Mais 6", and a
|
|
92
|
+
225px band of nothing. `computeSplit` now iterates to a fixed point: cheaper
|
|
93
|
+
furniture buys more filters, more filters push the ladder further down, a
|
|
94
|
+
further rung makes the furniture cheaper again, and the budget decreases
|
|
95
|
+
monotonically until it stops moving.
|
|
96
|
+
|
|
97
|
+
The furniture is also priced for what is ACTUALLY on the bar. "Exportar" exists
|
|
98
|
+
only when the host passes an `exportConfig` — several screens put their export
|
|
99
|
+
in the page header instead — and charging for the pair regardless billed the
|
|
100
|
+
filters ~108px for a button that was never rendered.
|
|
101
|
+
|
|
102
|
+
Which control survives on a phone is whichever fits the room left over, so a
|
|
103
|
+
large mobile keeps a cheaper pill where a tablet affords a wider one.
|
|
82
104
|
|
|
83
105
|
**An applied filter is ranked first, not exempt.** Applied controls take the
|
|
84
106
|
visible slots ahead of idle ones and go into "Mais" like anything else when even
|
|
@@ -133,7 +155,12 @@ Two distinct behaviours, and conflating them was a bug:
|
|
|
133
155
|
| --- | --- | --- | --- | --- | --- |
|
|
134
156
|
| Exibir / Exportar | icon | icon | label | label | label |
|
|
135
157
|
| dropdown chevron | no | no | yes | yes | yes |
|
|
136
|
-
| "N de N" counter | hidden |
|
|
158
|
+
| "N de N" counter | hidden | shown | shown | shown | shown |
|
|
159
|
+
|
|
160
|
+
The counter is priced at 56, not the old 96. It was measured against
|
|
161
|
+
"1.234 de 5.678"; a phone reads "3 de 3" and renders at 35px, so the extra
|
|
162
|
+
~60px was coming straight out of the filter budget at exactly the widths where
|
|
163
|
+
that budget is scarcest.
|
|
137
164
|
|
|
138
165
|
A tablet keeps the labels, which is worth stating because it is easy to assume
|
|
139
166
|
otherwise. With every filter already behind "Mais" the row has room to spare
|
|
@@ -176,6 +203,13 @@ leaves the board with a single populated column, which reads as data loss.
|
|
|
176
203
|
of the five widths and asserts the tables above. It is the executable copy of
|
|
177
204
|
this document; if the two disagree, the test is right and this needs updating.
|
|
178
205
|
|
|
206
|
+
`what fits follows the controls, not the width alone` guards the property the
|
|
207
|
+
rest of this document depends on: the counts above are OUTCOMES of pricing one
|
|
208
|
+
fixture against one row, never rules. At a single width it asserts that a table
|
|
209
|
+
declaring cheaper controls keeps more of them, that one long control keeps
|
|
210
|
+
none, and that the same table promotes another as its row grows — three answers
|
|
211
|
+
a breakpoint could not give.
|
|
212
|
+
|
|
179
213
|
Two things it cannot check, because jsdom has no layout engine: actual pixel
|
|
180
214
|
overflow, and how anything looks. Those are verified in a real browser against
|
|
181
215
|
the running Storybook, at 1600 / 1280 / 1024 / 900 / 768 / 600 / 500 / 430 / 390
|
|
@@ -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
|
|
@@ -142,6 +142,8 @@ interface GridShellFiltersArgs<T extends Record<string, unknown>> {
|
|
|
142
142
|
rangeFields: RangeFieldConfig<T>[];
|
|
143
143
|
testIdPrefix: string;
|
|
144
144
|
alwaysShowSearch: boolean;
|
|
145
|
+
/** Is "Exportar" on the bar? Half the right cluster's cost when it is not. */
|
|
146
|
+
hasExport: boolean;
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
function useGridShellFilters<T extends Record<string, unknown>>({
|
|
@@ -151,6 +153,7 @@ function useGridShellFilters<T extends Record<string, unknown>>({
|
|
|
151
153
|
rangeFields,
|
|
152
154
|
testIdPrefix,
|
|
153
155
|
alwaysShowSearch,
|
|
156
|
+
hasExport,
|
|
154
157
|
}: GridShellFiltersArgs<T>): {
|
|
155
158
|
showInline: boolean;
|
|
156
159
|
useModal: boolean;
|
|
@@ -171,6 +174,7 @@ function useGridShellFilters<T extends Record<string, unknown>>({
|
|
|
171
174
|
state.pills,
|
|
172
175
|
c.ranges,
|
|
173
176
|
openControls > 0,
|
|
177
|
+
hasExport,
|
|
174
178
|
);
|
|
175
179
|
// THE BAR RENDERS AT EVERY WIDTH. It used to be swapped for a full-screen
|
|
176
180
|
// filter MODAL below `lg`, which was the responsive strategy before the
|
|
@@ -365,7 +369,15 @@ export function GridShell<T extends Record<string, unknown>>(props: GridShellPro
|
|
|
365
369
|
inlineFilters = false,
|
|
366
370
|
alwaysShowSearch = false,
|
|
367
371
|
} = props;
|
|
368
|
-
const filters = useGridShellFilters({
|
|
372
|
+
const filters = useGridShellFilters({
|
|
373
|
+
c,
|
|
374
|
+
inlineFilters,
|
|
375
|
+
fields,
|
|
376
|
+
rangeFields,
|
|
377
|
+
testIdPrefix,
|
|
378
|
+
alwaysShowSearch,
|
|
379
|
+
hasExport: props.exportConfig !== undefined,
|
|
380
|
+
});
|
|
369
381
|
return (
|
|
370
382
|
<DataViewsLayoutProvider
|
|
371
383
|
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
|
+
}
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
estimateWidth,
|
|
7
|
+
furnitureCost,
|
|
8
|
+
GAP,
|
|
9
|
+
isRangeSet,
|
|
10
|
+
pillText,
|
|
11
|
+
RESERVED,
|
|
12
|
+
rightClusterCost,
|
|
13
|
+
} from "./data-views-overflow-costs";
|
|
6
14
|
import type { FilterFieldConfig, RangeFieldConfig, RangeValue } from "./data-views-types";
|
|
7
15
|
|
|
8
16
|
/**
|
|
@@ -154,6 +162,10 @@ function splitFilters<T extends Record<string, unknown>>(
|
|
|
154
162
|
pills: Record<string, string[]>,
|
|
155
163
|
ranges: Record<string, RangeValue>,
|
|
156
164
|
width: number,
|
|
165
|
+
/** Search + counter + right cluster + chrome, at the rung being considered. */
|
|
166
|
+
furniture: number,
|
|
167
|
+
/** Is the bar already down to icons? "Mais" shrinks with everything else. */
|
|
168
|
+
compact: boolean,
|
|
157
169
|
): Split<T> {
|
|
158
170
|
const none: OverflowField<T>[] = [];
|
|
159
171
|
const widthOf = (field: OverflowField<T>): number => fieldWidth(field, pills, ranges);
|
|
@@ -164,14 +176,15 @@ function splitFilters<T extends Record<string, unknown>>(
|
|
|
164
176
|
|
|
165
177
|
// "Limpar" rides the end of the cluster whenever anything is applied.
|
|
166
178
|
const clearCost = active.length > 0 ? RESERVED.clearAll + GAP : 0;
|
|
167
|
-
let available =
|
|
168
|
-
width - RESERVED.search - RESERVED.counter - RESERVED.right - RESERVED.chrome - clearCost;
|
|
179
|
+
let available = width - furniture - clearCost;
|
|
169
180
|
if (cost(all) <= available) {
|
|
170
181
|
return { inline: all, overflow: none, used: cost(all) + clearCost };
|
|
171
182
|
}
|
|
172
183
|
|
|
173
|
-
// There IS an overflow, so its button now costs room too
|
|
174
|
-
|
|
184
|
+
// There IS an overflow, so its button now costs room too — at the width it
|
|
185
|
+
// will actually have, which on a phone is the icon-and-badge one.
|
|
186
|
+
const overflowButton = compact ? RESERVED.overflowButtonCompact : RESERVED.overflowButton;
|
|
187
|
+
available -= overflowButton;
|
|
175
188
|
// APPLIED FIRST, BUT NOT EXEMPT.
|
|
176
189
|
//
|
|
177
190
|
// Applied controls take the visible slots ahead of idle ones — that part was
|
|
@@ -198,7 +211,7 @@ function splitFilters<T extends Record<string, unknown>>(
|
|
|
198
211
|
return {
|
|
199
212
|
inline,
|
|
200
213
|
overflow: all.filter((field) => !keep.has(field.id)),
|
|
201
|
-
used: cost(inline) +
|
|
214
|
+
used: cost(inline) + overflowButton + clearCost,
|
|
202
215
|
};
|
|
203
216
|
}
|
|
204
217
|
|
|
@@ -224,6 +237,8 @@ function computeSplit<T extends Record<string, unknown>>(
|
|
|
224
237
|
pills: Record<string, string[]>,
|
|
225
238
|
ranges: Record<string, RangeValue>,
|
|
226
239
|
width: number,
|
|
240
|
+
/** Does the host render "Exportar" beside "Exibir"? Half the cluster if not. */
|
|
241
|
+
hasExport: boolean,
|
|
227
242
|
): Omit<OverflowSplit<T>, "barRef"> {
|
|
228
243
|
// Unmeasured (SSR, or jsdom without a ResizeObserver) ⇒ degrade nothing.
|
|
229
244
|
if (width === 0) {
|
|
@@ -237,12 +252,47 @@ function computeSplit<T extends Record<string, unknown>>(
|
|
|
237
252
|
searchTakeover: false,
|
|
238
253
|
};
|
|
239
254
|
}
|
|
240
|
-
|
|
255
|
+
// Pass 1 prices the furniture at its widest — no rung has been taken yet.
|
|
256
|
+
let split = splitFilters(all, pills, ranges, width, furnitureCost(UNCOLLAPSED, hasExport), false);
|
|
257
|
+
let flags = ladderFlags(split, width, hasExport);
|
|
258
|
+
// RE-SPEND WHAT THE LATER RUNGS FREED.
|
|
259
|
+
//
|
|
260
|
+
// Rungs 2/4/5 turn 200 + 96 + 216 of furniture into 44 + 0 + 140, and until
|
|
261
|
+
// this loop existed nobody handed those ~330px back to the filters: the
|
|
262
|
+
// split was decided once, against the uncollapsed row, so a phone shed every
|
|
263
|
+
// control and then sat with a band of empty bar beside "Mais 5".
|
|
264
|
+
//
|
|
265
|
+
// Settles rather than oscillates, and cannot overfill: a cheaper furniture
|
|
266
|
+
// can only give the filters MORE room, more filter width can only push the
|
|
267
|
+
// ladder FURTHER down, and a further rung can only make the furniture
|
|
268
|
+
// cheaper again — so the budget decreases monotonically and the loop exits
|
|
269
|
+
// the moment it stops moving. The bound is a backstop, not the exit.
|
|
270
|
+
let budget = furnitureCost(UNCOLLAPSED, hasExport);
|
|
271
|
+
for (let pass = 0; pass < 3; pass += 1) {
|
|
272
|
+
const freed = furnitureCost(flags, hasExport);
|
|
273
|
+
if (freed >= budget) break;
|
|
274
|
+
budget = freed;
|
|
275
|
+
split = splitFilters(all, pills, ranges, width, budget, flags.compactControls);
|
|
276
|
+
flags = ladderFlags(split, width, hasExport);
|
|
277
|
+
}
|
|
278
|
+
return { inline: split.inline, overflow: split.overflow, ...flags };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The widest the furniture ever is — every rung still untaken. */
|
|
282
|
+
const UNCOLLAPSED = { searchCollapsed: false, counterHidden: false, compactControls: false };
|
|
283
|
+
|
|
284
|
+
/** Rungs 2 and 4-6, priced against what the filters actually took. */
|
|
285
|
+
function ladderFlags<T extends Record<string, unknown>>(
|
|
286
|
+
split: Split<T>,
|
|
287
|
+
width: number,
|
|
288
|
+
hasExport: boolean,
|
|
289
|
+
): Omit<OverflowSplit<T>, "barRef" | "inline" | "overflow"> {
|
|
290
|
+
const { overflow, used } = split;
|
|
241
291
|
const base = width - used - RESERVED.chrome;
|
|
242
292
|
|
|
243
293
|
// Would the search still make its minimum with the labels on?
|
|
244
|
-
const compactControls = base - RESERVED.counter -
|
|
245
|
-
const rightCost = compactControls
|
|
294
|
+
const compactControls = base - RESERVED.counter - rightClusterCost(hasExport, false) < RESERVED.search;
|
|
295
|
+
const rightCost = rightClusterCost(hasExport, compactControls);
|
|
246
296
|
// …and with them off?
|
|
247
297
|
const searchCollapsed = base - RESERVED.counter - rightCost < RESERVED.search;
|
|
248
298
|
// Everything has collapsed and the row STILL overflows. The controls cannot
|
|
@@ -258,26 +308,29 @@ function computeSplit<T extends Record<string, unknown>>(
|
|
|
258
308
|
// on there BEING an overflow, because with no panel the footer does not
|
|
259
309
|
// exist and dropping this would strand the operator.
|
|
260
310
|
const clearAllHidden = counterHidden && overflow.length > 0;
|
|
261
|
-
// What the keyword box would get if the operator expanded the magnifier
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
311
|
+
// What the keyword box would get if the operator expanded the magnifier.
|
|
312
|
+
// `searchCollapsed` is a different question — it asks whether the box fits at
|
|
313
|
+
// its PREFERRED 200px, and is deliberately pessimistic — so reusing it to
|
|
314
|
+
// decide the takeover evicted the filters on screens where the box had plenty
|
|
315
|
+
// of room to simply shrink.
|
|
316
|
+
//
|
|
317
|
+
// Asked with the inline filters SHED, because shedding them is exactly what
|
|
318
|
+
// expanding the box does: the row re-splits and they go behind "Mais".
|
|
319
|
+
// Measuring against the resting row instead made a filter argue for its own
|
|
320
|
+
// eviction — now that a large phone keeps one, its width came out of `base`,
|
|
321
|
+
// pushed the box under `usableSearch`, and tipped a row that had ample space
|
|
322
|
+
// to share into a full takeover.
|
|
323
|
+
const overflowButton = compactControls
|
|
324
|
+
? RESERVED.overflowButtonCompact
|
|
325
|
+
: RESERVED.overflowButton;
|
|
326
|
+
const shedRoom = width - RESERVED.chrome - (overflow.length > 0 ? overflowButton : 0);
|
|
327
|
+
const searchBoxRoom = shedRoom - (counterHidden ? 0 : RESERVED.counter) - rightCost;
|
|
267
328
|
// Below this a box is too narrow to read what you typed into it, and shrinking
|
|
268
329
|
// further buys nothing; that is the only point at which taking the whole
|
|
269
330
|
// cluster is worth losing the filters.
|
|
270
331
|
const searchTakeover = searchCollapsed && searchBoxRoom < RESERVED.usableSearch;
|
|
271
332
|
|
|
272
|
-
return {
|
|
273
|
-
inline,
|
|
274
|
-
overflow,
|
|
275
|
-
compactControls,
|
|
276
|
-
counterHidden,
|
|
277
|
-
searchCollapsed,
|
|
278
|
-
clearAllHidden,
|
|
279
|
-
searchTakeover,
|
|
280
|
-
};
|
|
333
|
+
return { compactControls, counterHidden, searchCollapsed, clearAllHidden, searchTakeover };
|
|
281
334
|
}
|
|
282
335
|
|
|
283
336
|
/**
|
|
@@ -307,12 +360,20 @@ export function useFilterOverflow<T extends Record<string, unknown>>(
|
|
|
307
360
|
* that is mid-edit.
|
|
308
361
|
*/
|
|
309
362
|
frozen = false,
|
|
363
|
+
/** Whether "Exportar" is on the bar — see `rightClusterCost`. */
|
|
364
|
+
hasExport = true,
|
|
310
365
|
): OverflowSplit<T> {
|
|
311
366
|
const { barRef, width } = useBarWidth();
|
|
312
|
-
const signature = JSON.stringify({
|
|
367
|
+
const signature = JSON.stringify({
|
|
368
|
+
ids: all.map((field) => field.id),
|
|
369
|
+
pills,
|
|
370
|
+
ranges,
|
|
371
|
+
width,
|
|
372
|
+
hasExport,
|
|
373
|
+
});
|
|
313
374
|
const cache = useRef<{ signature: string; split: ReturnType<typeof computeSplit<T>> } | null>(null);
|
|
314
375
|
if (cache.current === null || (!frozen && cache.current.signature !== signature)) {
|
|
315
|
-
cache.current = { signature, split: computeSplit(all, pills, ranges, width) };
|
|
376
|
+
cache.current = { signature, split: computeSplit(all, pills, ranges, width, hasExport) };
|
|
316
377
|
}
|
|
317
378
|
return { ...cache.current.split, barRef };
|
|
318
379
|
}
|