@12-apps/ui 1.6.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +1 -1
- package/src/components/data-display/DataViews/RESPONSIVE-BEHAVIOUR.md +183 -0
- package/src/components/data-display/DataViews/data-views-bar-controls.tsx +119 -0
- package/src/components/data-display/DataViews/data-views-display-panel.tsx +48 -22
- package/src/components/data-display/DataViews/data-views-display-tabs.tsx +64 -31
- package/src/components/data-display/DataViews/data-views-export.tsx +2 -1
- package/src/components/data-display/DataViews/data-views-glyphs.tsx +16 -0
- package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +5 -1
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +20 -14
- package/src/components/data-display/DataViews/data-views-inline-bar.tsx +134 -155
- package/src/components/data-display/DataViews/data-views-layout-context.tsx +21 -0
- package/src/components/data-display/DataViews/data-views-more-fields.tsx +291 -0
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +151 -199
- package/src/components/data-display/DataViews/data-views-overflow-costs.ts +100 -0
- package/src/components/data-display/DataViews/data-views-overflow.ts +89 -73
- package/src/components/data-display/DataViews/data-views-preset-chips.tsx +100 -0
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +27 -11
- package/src/components/data-display/DataViews/data-views-range-presets.ts +121 -0
- package/src/components/data-display/DataViews/data-views-range-types.ts +94 -0
- package/src/components/data-display/DataViews/data-views-scope-tabs.tsx +57 -0
- package/src/components/data-display/DataViews/data-views-scopes.tsx +2 -2
- package/src/components/data-display/DataViews/data-views-search.tsx +19 -1
- package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +4 -0
- package/src/components/data-display/DataViews/data-views-toolbar.tsx +31 -21
- package/src/components/data-display/DataViews/data-views-types.ts +14 -47
- package/src/components/data-display/DataViews/index.ts +1 -0
- package/src/components/data-display/DataViews/pedidos.server.ts +118 -0
- package/src/components/data-display/DataViews/use-data-views-state.ts +1 -1
- package/src/components/layout/ContentToolbar/ContentToolbar.tsx +16 -1
- package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +9 -1
- package/src/components/layout/ContentToolbar/MultiSelectTriggers.tsx +39 -1
|
@@ -14,7 +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 {
|
|
17
|
+
import { InlineFilterControls } from "./data-views-inline-bar";
|
|
18
18
|
import { toOverflowFields, useFilterOverflow, type OverflowSplit } from "./data-views-overflow";
|
|
19
19
|
import { ShellToolbar } from "./data-views-shell-toolbar";
|
|
20
20
|
import type { DisplayPanelView } from "./data-views-display-panel";
|
|
@@ -22,7 +22,8 @@ import type { DataViewExport } from "./data-views-export";
|
|
|
22
22
|
import { DataViewsEmpty } from "./data-views-empty";
|
|
23
23
|
import { DataViewsPagination } from "./data-views-pagination";
|
|
24
24
|
import type { BoardConfig } from "./DataViewsBoard";
|
|
25
|
-
import {
|
|
25
|
+
import { type ScopeConfig } from "./data-views-scopes";
|
|
26
|
+
import { ScopeTabsSlot } from "./data-views-scope-tabs";
|
|
26
27
|
import { togglePillValues } from "./data-views-grid-helpers";
|
|
27
28
|
import type {
|
|
28
29
|
DataViewCardSelection,
|
|
@@ -233,8 +234,13 @@ interface GridShellProps<T extends Record<string, unknown>> {
|
|
|
233
234
|
board?: BoardConfig<T>;
|
|
234
235
|
/** Opt-in "Lista" layout: one full-width, entity-rendered row per record. */
|
|
235
236
|
renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
236
|
-
/** The page-level partition rendered as tabs
|
|
237
|
+
/** The page-level partition rendered as tabs under the toolbar. */
|
|
237
238
|
scopes?: ScopeConfig[];
|
|
239
|
+
/**
|
|
240
|
+
* The row field the scopes partition by. Compared against the board's
|
|
241
|
+
* `groupBy` so the two are not shown as the same partition twice.
|
|
242
|
+
*/
|
|
243
|
+
scopeFieldId?: string;
|
|
238
244
|
/** Per-sort-field value kind, so directions read in the column's own terms. */
|
|
239
245
|
sortKinds?: Record<string, string>;
|
|
240
246
|
/** The saved-view chrome bracketing the Exibir panel. */
|
|
@@ -289,14 +295,6 @@ function ShellStack<T extends Record<string, unknown>>({
|
|
|
289
295
|
<TableFilter open={c.filterOpen} onOpenChange={c.setFilterOpen} hasActiveFilters={c.activeFilterCount > 0}>
|
|
290
296
|
<Stack spacing={0} data-testid={dataTestId ? `${dataTestId}-container` : undefined}>
|
|
291
297
|
<GridHeaderRow title={props.title} headerActions={props.headerActions} testIdPrefix={testIdPrefix} />
|
|
292
|
-
{/* Renders nothing (and reserves nothing) for an empty scope list. */}
|
|
293
|
-
<DataViewsScopeTabs
|
|
294
|
-
scopes={scopes}
|
|
295
|
-
value={c.scope}
|
|
296
|
-
onChange={c.setScope}
|
|
297
|
-
counts={c.scopeCounts}
|
|
298
|
-
testIdPrefix={testIdPrefix}
|
|
299
|
-
/>
|
|
300
298
|
<ShellToolbar
|
|
301
299
|
c={c}
|
|
302
300
|
rows={props.rows}
|
|
@@ -308,6 +306,7 @@ function ShellStack<T extends Record<string, unknown>>({
|
|
|
308
306
|
<InlineFilterControls
|
|
309
307
|
{...filterProps}
|
|
310
308
|
split={split}
|
|
309
|
+
activeFilterCount={c.activeFilterCount}
|
|
311
310
|
onControlOpenChange={onControlOpenChange}
|
|
312
311
|
/>
|
|
313
312
|
) : undefined
|
|
@@ -318,11 +317,18 @@ function ShellStack<T extends Record<string, unknown>>({
|
|
|
318
317
|
bulkActions={props.bulkActions}
|
|
319
318
|
toolbarRightSlot={props.toolbarRightSlot}
|
|
320
319
|
compactControls={showInline && split.compactControls}
|
|
320
|
+
counterHidden={showInline && split.counterHidden}
|
|
321
321
|
showInline={showInline}
|
|
322
322
|
/>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
<ScopeTabsSlot
|
|
324
|
+
scopes={scopes}
|
|
325
|
+
scopeFieldId={props.scopeFieldId}
|
|
326
|
+
board={props.board}
|
|
327
|
+
value={c.scope}
|
|
328
|
+
onChange={c.setScope}
|
|
329
|
+
counts={c.scopeCounts}
|
|
330
|
+
testIdPrefix={testIdPrefix}
|
|
331
|
+
/>
|
|
326
332
|
<TableFilter.Layout>
|
|
327
333
|
<TableFilter.Main>
|
|
328
334
|
<GridMain
|
|
@@ -3,24 +3,29 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* The INLINE filter bar — the horizontal row of controls a grid renders instead
|
|
5
5
|
* of the slide-in panel on a wide screen (`inlineFilters`): the compact keyword
|
|
6
|
-
* box, one pill per facet, one pill per range, and
|
|
7
|
-
*
|
|
6
|
+
* box, one pill per facet, one pill per range, and "Mais N" for whatever had no
|
|
7
|
+
* room.
|
|
8
|
+
*
|
|
9
|
+
* There is no "Filtros ativos" row beneath it. Listing every applied filter a
|
|
10
|
+
* second time, under the controls already showing them, was the same
|
|
11
|
+
* information twice — and put the ✕ that removes a filter somewhere other than
|
|
12
|
+
* the control that applied it. Each pill carries its own.
|
|
8
13
|
*
|
|
9
14
|
* Split from `data-views-filter-panel` because they are two surfaces, not one:
|
|
10
15
|
* the panel is a column of stacked fields, this is a row of pills, and the only
|
|
11
16
|
* thing they share is the prop shape they are both driven by.
|
|
12
17
|
*/
|
|
13
|
-
import { Box
|
|
18
|
+
import { Box } from "@mui/material";
|
|
14
19
|
import { useState } from "react";
|
|
15
20
|
|
|
16
21
|
import { MultiSelectDropdown } from "../../layout/ContentToolbar";
|
|
17
22
|
|
|
23
|
+
import { ClearAllControl, CloseSearchControl } from "./data-views-bar-controls";
|
|
18
24
|
import type { GridFilterPanelProps } from "./data-views-filter-panel";
|
|
19
25
|
import { MoreFilters } from "./data-views-more-filters";
|
|
20
26
|
import type { OverflowField, OverflowSplit } from "./data-views-overflow";
|
|
21
27
|
import { CollapsedSearch, InlineKeyword } from "./data-views-search";
|
|
22
28
|
import { RangePill } from "./data-views-range-pill";
|
|
23
|
-
import { isRangeSet, rangeChipLabel } from "./data-views-range-values";
|
|
24
29
|
import type { RangeValue } from "./data-views-types";
|
|
25
30
|
|
|
26
31
|
|
|
@@ -33,6 +38,7 @@ function SearchSlot({
|
|
|
33
38
|
onSearchChange,
|
|
34
39
|
collapsed,
|
|
35
40
|
expanded,
|
|
41
|
+
fill,
|
|
36
42
|
onOpen,
|
|
37
43
|
onEscape,
|
|
38
44
|
testIdPrefix,
|
|
@@ -41,6 +47,8 @@ function SearchSlot({
|
|
|
41
47
|
onSearchChange: (value: string) => void;
|
|
42
48
|
collapsed: boolean;
|
|
43
49
|
expanded: boolean;
|
|
50
|
+
/** The box owns the cluster alone, so it may shrink below its usual floor. */
|
|
51
|
+
fill: boolean;
|
|
44
52
|
onOpen: () => void;
|
|
45
53
|
onEscape: () => void;
|
|
46
54
|
testIdPrefix: string;
|
|
@@ -57,6 +65,7 @@ function SearchSlot({
|
|
|
57
65
|
testId={`${testIdPrefix}-search-all`}
|
|
58
66
|
autoFocus={expanded}
|
|
59
67
|
onEscape={onEscape}
|
|
68
|
+
fill={fill}
|
|
60
69
|
/>
|
|
61
70
|
);
|
|
62
71
|
}
|
|
@@ -80,70 +89,6 @@ type InlineFilterBarProps<T extends Record<string, unknown>> = Pick<
|
|
|
80
89
|
split: OverflowSplit<T>;
|
|
81
90
|
};
|
|
82
91
|
|
|
83
|
-
/** One removable active-filter chip: a search term, a pill value or a window. */
|
|
84
|
-
interface ActiveChip {
|
|
85
|
-
key: string;
|
|
86
|
-
label: string;
|
|
87
|
-
onDelete: () => void;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Every applied filter flattened into removable chips. A pill contributes one
|
|
92
|
-
* chip per selected value (deleting one leaves the rest), while a RANGE
|
|
93
|
-
* contributes a single chip for the whole window — "remove the period" means
|
|
94
|
-
* both ends, not an arbitrary one.
|
|
95
|
-
*/
|
|
96
|
-
function activeChips<T extends Record<string, unknown>>({
|
|
97
|
-
search,
|
|
98
|
-
fields,
|
|
99
|
-
rangeFields,
|
|
100
|
-
pills,
|
|
101
|
-
ranges,
|
|
102
|
-
onSearchChange,
|
|
103
|
-
onTogglePill,
|
|
104
|
-
onChangeRange,
|
|
105
|
-
}: Pick<
|
|
106
|
-
GridFilterPanelProps<T>,
|
|
107
|
-
| "search"
|
|
108
|
-
| "fields"
|
|
109
|
-
| "rangeFields"
|
|
110
|
-
| "pills"
|
|
111
|
-
| "ranges"
|
|
112
|
-
| "onSearchChange"
|
|
113
|
-
| "onTogglePill"
|
|
114
|
-
| "onChangeRange"
|
|
115
|
-
>): ActiveChip[] {
|
|
116
|
-
const searchChip: ActiveChip[] =
|
|
117
|
-
search.trim() === ""
|
|
118
|
-
? []
|
|
119
|
-
: [{ key: "__search", label: `Busca: ${search}`, onDelete: () => onSearchChange("") }];
|
|
120
|
-
const pillChips: ActiveChip[] = fields.flatMap((field) =>
|
|
121
|
-
(pills[field.id] ?? []).map((value) => ({
|
|
122
|
-
key: `${field.id}:${value}`,
|
|
123
|
-
label: `${field.label}: ${field.options.find((option) => option.value === value)?.label ?? value}`,
|
|
124
|
-
onDelete: () => onTogglePill(field.id, value, false),
|
|
125
|
-
})),
|
|
126
|
-
);
|
|
127
|
-
const rangeChips: ActiveChip[] = rangeFields.flatMap((field) => {
|
|
128
|
-
const range = ranges[field.id];
|
|
129
|
-
if (!range || !isRangeSet(range)) return [];
|
|
130
|
-
return [
|
|
131
|
-
{
|
|
132
|
-
key: `range:${field.id}`,
|
|
133
|
-
label: rangeChipLabel(field, range),
|
|
134
|
-
onDelete: () => onChangeRange(field.id, {}),
|
|
135
|
-
},
|
|
136
|
-
];
|
|
137
|
-
});
|
|
138
|
-
return [...searchChip, ...pillChips, ...rangeChips];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* A horizontal filter bar (used instead of the slide-in {@link GridFilterPanel} on
|
|
143
|
-
* wide screens): the compact keyword search, each field as a rounded pill dropdown,
|
|
144
|
-
* and — when anything is applied — a row of removable "active filter" chips. Range
|
|
145
|
-
* filters are not shown here (the slide-in panel keeps those).
|
|
146
|
-
*/
|
|
147
92
|
/**
|
|
148
93
|
* One filter control on the bar: a multi-select pill or a min/max range. The
|
|
149
94
|
* two are the same thing from the bar's point of view — a labelled control that
|
|
@@ -201,6 +146,46 @@ function InlineControl<T extends Record<string, unknown>>({
|
|
|
201
146
|
return null;
|
|
202
147
|
}
|
|
203
148
|
|
|
149
|
+
/**
|
|
150
|
+
* WHICH OF THE SEARCH'S THREE STATES THE BAR IS IN.
|
|
151
|
+
*
|
|
152
|
+
* Resting, expanded-and-sharing, or expanded-and-owning-the-cluster. They were
|
|
153
|
+
* one boolean until a phone showed why they are not:
|
|
154
|
+
*
|
|
155
|
+
* - `fill` — an expanded box drops its usual 200px floor and takes whatever the
|
|
156
|
+
* cluster has. That floor is right for a box that lives on the row; it is
|
|
157
|
+
* wrong for one expanded into a cluster the ladder sized for an icon, where
|
|
158
|
+
* insisting on it made the row scroll sideways (154px of overhang at 320px,
|
|
159
|
+
* 84px at 390px, 42px at 500px).
|
|
160
|
+
* - `takesOver` — the narrower case, where shrinking has run out of road and
|
|
161
|
+
* what is left would be too small to read. Only then do the filters stand
|
|
162
|
+
* down. A large phone has room to share, and evicting them there cost the
|
|
163
|
+
* operator their filters for nothing.
|
|
164
|
+
*
|
|
165
|
+
* Once CLICKED the box stays open until Escape or its ✕, so typing is never
|
|
166
|
+
* interrupted by a re-measure.
|
|
167
|
+
*/
|
|
168
|
+
function useSearchMode<T extends Record<string, unknown>>(
|
|
169
|
+
split: OverflowSplit<T>,
|
|
170
|
+
): {
|
|
171
|
+
showBox: boolean;
|
|
172
|
+
expanded: boolean;
|
|
173
|
+
fill: boolean;
|
|
174
|
+
takesOver: boolean;
|
|
175
|
+
open: () => void;
|
|
176
|
+
close: () => void;
|
|
177
|
+
} {
|
|
178
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
179
|
+
return {
|
|
180
|
+
showBox: !split.searchCollapsed || searchOpen,
|
|
181
|
+
expanded: searchOpen,
|
|
182
|
+
fill: split.searchCollapsed && searchOpen,
|
|
183
|
+
takesOver: searchOpen && split.searchTakeover,
|
|
184
|
+
open: () => setSearchOpen(true),
|
|
185
|
+
close: () => setSearchOpen(false),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
204
189
|
/**
|
|
205
190
|
* The filter CONTROLS alone — search, the fields that fit, and "Mais N" for the
|
|
206
191
|
* ones that don't — with no row chrome of its own, because it is rendered ON
|
|
@@ -219,8 +204,12 @@ export function InlineFilterControls<T extends Record<string, unknown>>({
|
|
|
219
204
|
onChangeRange,
|
|
220
205
|
onClearField,
|
|
221
206
|
split,
|
|
207
|
+
onClearAll,
|
|
208
|
+
activeFilterCount,
|
|
222
209
|
onControlOpenChange,
|
|
223
|
-
}: Omit<InlineFilterBarProps<T>, "fields" | "rangeFields"
|
|
210
|
+
}: Omit<InlineFilterBarProps<T>, "fields" | "rangeFields"> & {
|
|
211
|
+
/** How many filters are applied — decides whether "Limpar" is on the bar at all. */
|
|
212
|
+
activeFilterCount: number;
|
|
224
213
|
/** Reports a control opening/closing so the shell can freeze the measurement. */
|
|
225
214
|
onControlOpenChange?: (open: boolean) => void;
|
|
226
215
|
}): React.JSX.Element {
|
|
@@ -230,11 +219,8 @@ export function InlineFilterControls<T extends Record<string, unknown>>({
|
|
|
230
219
|
// `RESERVED` prices the counter and Exibir/Exportar, so measuring only the
|
|
231
220
|
// controls would charge for that furniture a second time and collapse the
|
|
232
221
|
// ladder while the row still had hundreds of free pixels.
|
|
233
|
-
const { inline, overflow,
|
|
234
|
-
|
|
235
|
-
// until Escape, so typing is never interrupted by a re-measure.
|
|
236
|
-
const [searchOpen, setSearchOpen] = useState(false);
|
|
237
|
-
const showBox = !searchCollapsed || searchOpen;
|
|
222
|
+
const { inline, overflow, compactControls, clearAllHidden } = split;
|
|
223
|
+
const { showBox, expanded, fill, takesOver, open, close } = useSearchMode(split);
|
|
238
224
|
return (
|
|
239
225
|
<Box
|
|
240
226
|
data-testid={`${testIdPrefix}-inline-filters`}
|
|
@@ -253,111 +239,104 @@ export function InlineFilterControls<T extends Record<string, unknown>>({
|
|
|
253
239
|
search={search}
|
|
254
240
|
onSearchChange={onSearchChange}
|
|
255
241
|
collapsed={!showBox}
|
|
256
|
-
expanded={
|
|
257
|
-
|
|
258
|
-
|
|
242
|
+
expanded={expanded}
|
|
243
|
+
fill={fill}
|
|
244
|
+
onOpen={open}
|
|
245
|
+
onEscape={close}
|
|
259
246
|
testIdPrefix={testIdPrefix}
|
|
260
247
|
/>
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
pills={pills}
|
|
267
|
-
ranges={ranges}
|
|
268
|
-
onTogglePill={onTogglePill}
|
|
269
|
-
onChangeRange={onChangeRange}
|
|
270
|
-
onClearField={onClearField}
|
|
271
|
-
testIdPrefix={testIdPrefix}
|
|
272
|
-
/>
|
|
273
|
-
))}
|
|
274
|
-
<MoreFilters
|
|
275
|
-
fields={overflow}
|
|
248
|
+
{takesOver && <CloseSearchControl onClose={close} testIdPrefix={testIdPrefix} />}
|
|
249
|
+
{!takesOver && (
|
|
250
|
+
<FilterSlots
|
|
251
|
+
inline={inline}
|
|
252
|
+
overflow={overflow}
|
|
276
253
|
pills={pills}
|
|
277
254
|
ranges={ranges}
|
|
278
255
|
onTogglePill={onTogglePill}
|
|
279
256
|
onChangeRange={onChangeRange}
|
|
257
|
+
onClearField={onClearField}
|
|
258
|
+
onClearAll={onClearAll}
|
|
259
|
+
onControlOpenChange={onControlOpenChange}
|
|
260
|
+
activeFilterCount={activeFilterCount}
|
|
261
|
+
compactControls={compactControls}
|
|
262
|
+
clearAllHidden={clearAllHidden}
|
|
280
263
|
testIdPrefix={testIdPrefix}
|
|
281
264
|
/>
|
|
265
|
+
)}
|
|
282
266
|
</Box>
|
|
283
267
|
);
|
|
284
268
|
}
|
|
285
269
|
|
|
286
270
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
271
|
+
* Everything to the right of the search: the fields that fit, "Mais N" for the
|
|
272
|
+
* ones that don't, and "Limpar". Split out of `InlineFilterControls` only to
|
|
273
|
+
* keep that function inside the size gate — the three are one cluster and the
|
|
274
|
+
* order they render in is the order they shed in.
|
|
290
275
|
*/
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
fields,
|
|
295
|
-
rangeFields,
|
|
276
|
+
function FilterSlots<T extends Record<string, unknown>>({
|
|
277
|
+
inline,
|
|
278
|
+
overflow,
|
|
296
279
|
pills,
|
|
297
280
|
ranges,
|
|
298
|
-
onSearchChange,
|
|
299
281
|
onTogglePill,
|
|
300
282
|
onChangeRange,
|
|
283
|
+
onClearField,
|
|
301
284
|
onClearAll,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
rangeFields,
|
|
307
|
-
pills,
|
|
308
|
-
ranges,
|
|
309
|
-
onSearchChange,
|
|
310
|
-
onTogglePill,
|
|
311
|
-
onChangeRange,
|
|
312
|
-
});
|
|
313
|
-
if (chips.length === 0) return null;
|
|
314
|
-
return (
|
|
315
|
-
<Box sx={{ py: 1.5 }}>
|
|
316
|
-
<ActiveChipRow chips={chips} testIdPrefix={testIdPrefix} onClearAll={onClearAll} />
|
|
317
|
-
</Box>
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* The "Filtros ativos:" row — one removable chip per applied filter plus the
|
|
323
|
-
* single "Limpar" that drops them all. Renders nothing when nothing is applied,
|
|
324
|
-
* so the bar collapses to its controls.
|
|
325
|
-
*/
|
|
326
|
-
function ActiveChipRow({
|
|
327
|
-
chips,
|
|
285
|
+
onControlOpenChange,
|
|
286
|
+
activeFilterCount,
|
|
287
|
+
compactControls,
|
|
288
|
+
clearAllHidden,
|
|
328
289
|
testIdPrefix,
|
|
329
|
-
onClearAll,
|
|
330
290
|
}: {
|
|
331
|
-
|
|
332
|
-
|
|
291
|
+
inline: OverflowField<T>[];
|
|
292
|
+
overflow: OverflowField<T>[];
|
|
293
|
+
pills: Record<string, string[]>;
|
|
294
|
+
ranges: Record<string, RangeValue>;
|
|
295
|
+
onTogglePill: (fieldId: string, value: string, checked: boolean) => void;
|
|
296
|
+
onChangeRange: (fieldId: string, range: RangeValue) => void;
|
|
297
|
+
onClearField: (fieldId: string) => void;
|
|
333
298
|
onClearAll: () => void;
|
|
334
|
-
|
|
335
|
-
|
|
299
|
+
onControlOpenChange?: (open: boolean) => void;
|
|
300
|
+
activeFilterCount: number;
|
|
301
|
+
compactControls: boolean;
|
|
302
|
+
clearAllHidden: boolean;
|
|
303
|
+
testIdPrefix: string;
|
|
304
|
+
}): React.JSX.Element {
|
|
336
305
|
return (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
306
|
+
<>
|
|
307
|
+
{inline.map((field) => (
|
|
308
|
+
<InlineControl
|
|
309
|
+
key={field.id}
|
|
310
|
+
field={field}
|
|
311
|
+
onOpenChange={onControlOpenChange}
|
|
312
|
+
pills={pills}
|
|
313
|
+
ranges={ranges}
|
|
314
|
+
onTogglePill={onTogglePill}
|
|
315
|
+
onChangeRange={onChangeRange}
|
|
316
|
+
onClearField={onClearField}
|
|
317
|
+
testIdPrefix={testIdPrefix}
|
|
349
318
|
/>
|
|
350
319
|
))}
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
320
|
+
<MoreFilters
|
|
321
|
+
onOpenChange={onControlOpenChange}
|
|
322
|
+
onClearAll={onClearAll}
|
|
323
|
+
anyApplied={activeFilterCount > 0}
|
|
324
|
+
compact={compactControls}
|
|
325
|
+
fields={overflow}
|
|
326
|
+
pills={pills}
|
|
327
|
+
ranges={ranges}
|
|
328
|
+
onTogglePill={onTogglePill}
|
|
329
|
+
onChangeRange={onChangeRange}
|
|
330
|
+
testIdPrefix={testIdPrefix}
|
|
331
|
+
/>
|
|
332
|
+
{activeFilterCount > 0 && !clearAllHidden && (
|
|
333
|
+
<ClearAllControl
|
|
334
|
+
onClearAll={onClearAll}
|
|
335
|
+
compact={compactControls}
|
|
336
|
+
testIdPrefix={testIdPrefix}
|
|
337
|
+
/>
|
|
338
|
+
)}
|
|
339
|
+
</>
|
|
362
340
|
);
|
|
363
341
|
}
|
|
342
|
+
|
|
@@ -88,6 +88,27 @@ export const DENSITY_CARD_COLUMNS: Record<DataViewsDensity, number> = {
|
|
|
88
88
|
comfortable: 3,
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The BOARD's column-width multiplier per density.
|
|
93
|
+
*
|
|
94
|
+
* Density means "how much air a record gets", and on a board that is the column
|
|
95
|
+
* width — narrow columns fit more etapas on screen, wide ones give each card
|
|
96
|
+
* room to be read. Without this the board was the one layout the Exibição tab
|
|
97
|
+
* offered nothing for: the format tile switched to Quadro and the whole density
|
|
98
|
+
* section vanished, which reads as a control that broke rather than one that
|
|
99
|
+
* does not apply.
|
|
100
|
+
*
|
|
101
|
+
* The spread is deliberately narrower than the card grid's. A board column has
|
|
102
|
+
* a hard floor (`MIN_COLUMN_WIDTH` — a card below it stops being readable), so
|
|
103
|
+
* a multiplier that undershoots it buys nothing and makes "Estreita" a tile
|
|
104
|
+
* that does nothing on a phone.
|
|
105
|
+
*/
|
|
106
|
+
export const DENSITY_BOARD_SCALE: Record<DataViewsDensity, number> = {
|
|
107
|
+
compact: 0.95,
|
|
108
|
+
cozy: 1,
|
|
109
|
+
comfortable: 1.25,
|
|
110
|
+
};
|
|
111
|
+
|
|
91
112
|
/**
|
|
92
113
|
* Map the 0–100 zoom to the card SIZE multiplier. This is the single knob: the
|
|
93
114
|
* card's width is {@link BASE_CARD_WIDTH} × scale and its padding + typography
|