@12-apps/ui 1.5.2 → 1.6.1

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 (44) hide show
  1. package/package.json +4 -4
  2. package/src/components/data-display/DataViews/DataViewsBoard.tsx +327 -0
  3. package/src/components/data-display/DataViews/DataViewsGrid.tsx +80 -77
  4. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +178 -230
  5. package/src/components/data-display/DataViews/DeleteViewDialog.tsx +83 -0
  6. package/src/components/data-display/DataViews/SaveViewModal.tsx +140 -52
  7. package/src/components/data-display/DataViews/data-views-columns-tab.tsx +240 -0
  8. package/src/components/data-display/DataViews/data-views-columns.ts +129 -0
  9. package/src/components/data-display/DataViews/data-views-dirty.ts +62 -0
  10. package/src/components/data-display/DataViews/data-views-display-panel.tsx +328 -0
  11. package/src/components/data-display/DataViews/data-views-display-tabs.tsx +328 -0
  12. package/src/components/data-display/DataViews/data-views-empty.tsx +91 -0
  13. package/src/components/data-display/DataViews/data-views-export.tsx +260 -0
  14. package/src/components/data-display/DataViews/data-views-glyphs.tsx +103 -0
  15. package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +340 -0
  16. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +220 -242
  17. package/src/components/data-display/DataViews/data-views-inline-bar.tsx +187 -81
  18. package/src/components/data-display/DataViews/data-views-layout-context.tsx +215 -79
  19. package/src/components/data-display/DataViews/data-views-more-filters.tsx +300 -0
  20. package/src/components/data-display/DataViews/data-views-overflow.ts +302 -0
  21. package/src/components/data-display/DataViews/data-views-preview.ts +11 -3
  22. package/src/components/data-display/DataViews/data-views-range-pill.tsx +162 -92
  23. package/src/components/data-display/DataViews/data-views-range-values.ts +69 -0
  24. package/src/components/data-display/DataViews/data-views-scopes.tsx +356 -0
  25. package/src/components/data-display/DataViews/data-views-search.tsx +156 -0
  26. package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +105 -0
  27. package/src/components/data-display/DataViews/data-views-selection.ts +50 -0
  28. package/src/components/data-display/DataViews/data-views-server-query.ts +75 -0
  29. package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +87 -0
  30. package/src/components/data-display/DataViews/data-views-table-parts.tsx +5 -0
  31. package/src/components/data-display/DataViews/data-views-toolbar.tsx +63 -48
  32. package/src/components/data-display/DataViews/data-views-types.ts +88 -9
  33. package/src/components/data-display/DataViews/data-views-view-nav.tsx +378 -0
  34. package/src/components/data-display/DataViews/index.ts +14 -1
  35. package/src/components/data-display/DataViews/use-data-views-state.ts +158 -134
  36. package/src/components/data-display/DataViews/use-saved-views-controller.ts +171 -0
  37. package/src/components/layout/ContentToolbar/ContentToolbar.tsx +86 -30
  38. package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +25 -0
  39. package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +6 -1
  40. package/src/components/utility/InstallPrompt/InstallPrompt.earlyCapture.ts +117 -0
  41. package/src/components/utility/InstallPrompt/InstallPrompt.md +52 -4
  42. package/src/components/utility/InstallPrompt/InstallPrompt.types.ts +19 -0
  43. package/src/components/utility/InstallPrompt/index.ts +8 -0
  44. package/src/components/utility/InstallPrompt/usePwaInstall.ts +39 -11
@@ -0,0 +1,340 @@
1
+ "use client";
2
+
3
+ import { DataGrid, type GridColumn, type GridSort } from "../DataGrid";
4
+ import { Box } from "../../../mui/Box";
5
+
6
+ import {
7
+ cardMinWidthForZoom,
8
+ cardScaleForZoom,
9
+ DENSITY_CARD_COLUMNS,
10
+ DENSITY_ROW_PADDING,
11
+ useDataViewsLayout,
12
+ } from "./data-views-layout-context";
13
+ import { DataViewsBoard, type BoardConfig } from "./DataViewsBoard";
14
+ import { SelectAllStrip } from "./data-views-select-all-strip";
15
+ import type { DataViewCardSelection } from "./data-views-types";
16
+ import type { DataViewsController } from "./use-data-views-state";
17
+
18
+ /* ── Body (grid) ─────────────────────────────────────────────────────────── */
19
+
20
+ interface GridBodyProps<T extends Record<string, unknown>> {
21
+ rows: T[];
22
+ columns: GridColumn<T>[];
23
+ getRowId: (row: T) => string | number;
24
+ selectedIds: Array<string | number>;
25
+ onChangeSelected: (ids: Array<string | number>) => void;
26
+ sortBy: GridSort[];
27
+ onChangeSortBy: (next: GridSort[]) => void;
28
+ /** "server" defers ordering to the backend (rows render as-is); "client" sorts in-grid. */
29
+ sortMode: "client" | "server";
30
+ /** Vertical cell padding, from the density preference. */
31
+ rowPadding: number;
32
+ dataTestId?: string;
33
+ emptyState?: React.ReactNode;
34
+ }
35
+
36
+ /** The dense DataGrid with multi-select, wrapped in the scrollable table region. */
37
+ function GridBody<T extends Record<string, unknown>>({
38
+ rows,
39
+ columns,
40
+ getRowId,
41
+ selectedIds,
42
+ onChangeSelected,
43
+ sortBy,
44
+ onChangeSortBy,
45
+ sortMode,
46
+ rowPadding,
47
+ dataTestId,
48
+ emptyState,
49
+ }: GridBodyProps<T>): React.JSX.Element {
50
+ return (
51
+ <Box
52
+ sx={{
53
+ width: "100%",
54
+ overflowX: "auto",
55
+ mt: 1.5,
56
+ // Tabwoah-style dense rows: MUI's default TableCell padding keeps rows
57
+ // tall regardless of rowHeight, so the DENSITY preference is applied
58
+ // here rather than through `rowHeight`, which it would fight.
59
+ "& .MuiTableCell-root": { py: rowPadding, fontSize: "0.8125rem" },
60
+ "& .MuiTableCell-head": { py: 0.5, fontSize: "0.75rem" },
61
+ }}
62
+ >
63
+ <DataGrid<T>
64
+ rows={rows}
65
+ columns={columns}
66
+ getRowId={(row) => getRowId(row)}
67
+ virtualizeRows={false}
68
+ density="compact"
69
+ rowHeight={36}
70
+ headerHeight={36}
71
+ selection={{ mode: "multi", selectedRowIds: selectedIds, onChangeSelected }}
72
+ sorting={{ mode: sortMode, sortBy, onChangeSortBy }}
73
+ data-testid={dataTestId}
74
+ emptyState={emptyState}
75
+ />
76
+ </Box>
77
+ );
78
+ }
79
+
80
+ /* ── Body (cards) ────────────────────────────────────────────────────────── */
81
+
82
+ interface CardBodyProps<T extends Record<string, unknown>> {
83
+ rows: T[];
84
+ renderCard: (row: T, selection: DataViewCardSelection) => React.ReactNode;
85
+ getRowId: (row: T) => string | number;
86
+ selectedIds: Set<string | number>;
87
+ onToggleId: (id: string | number) => void;
88
+ minCardWidth: number;
89
+ /** Cap on cards per row, from the density preference. */
90
+ maxColumns: number;
91
+ /** Content scale (padding + type) handed to each card, from the zoom slider. */
92
+ cardScale: number;
93
+ dataTestId?: string;
94
+ emptyState?: React.ReactNode;
95
+ }
96
+
97
+ /**
98
+ * The "Grade" layout: the filtered/sorted rows rendered as an auto-filling grid
99
+ * of entity-supplied cards. Reuses `rows` (= `c.matched`), so search/filter/sort
100
+ * apply; the column width comes from the zoom slider, and each card is handed its
101
+ * selection state so it can drive its own checkbox (BaseCard) — the same
102
+ * selection model as the table.
103
+ */
104
+ function CardBody<T extends Record<string, unknown>>({
105
+ rows,
106
+ renderCard,
107
+ getRowId,
108
+ selectedIds,
109
+ onToggleId,
110
+ minCardWidth,
111
+ maxColumns,
112
+ cardScale,
113
+ dataTestId,
114
+ emptyState,
115
+ }: CardBodyProps<T>): React.JSX.Element {
116
+ if (rows.length === 0) {
117
+ return <Box sx={{ mt: 1.5 }}>{emptyState}</Box>;
118
+ }
119
+ return (
120
+ <Box
121
+ sx={{
122
+ mt: 1.5,
123
+ display: "grid",
124
+ // Fixed inter-card gap — deliberately NOT scaled by the zoom slider, so
125
+ // only the cards grow while the space between them stays constant.
126
+ gap: 1.5,
127
+ // `auto-fill` still decides how many FIT; density caps how many are
128
+ // ALLOWED, so "Poucos" reads as bigger cards rather than more of them.
129
+ gridTemplateColumns: {
130
+ xs: "1fr",
131
+ sm: `repeat(auto-fill, minmax(max(${minCardWidth}px, calc((100% - ${(maxColumns - 1) * 12}px) / ${maxColumns})), 1fr))`,
132
+ },
133
+ }}
134
+ data-testid={dataTestId ? `${dataTestId}-cards` : "data-views-cards"}
135
+ >
136
+ {rows.map((row) => {
137
+ const id = getRowId(row);
138
+ return (
139
+ <Box key={id}>
140
+ {renderCard(row, {
141
+ selected: selectedIds.has(id),
142
+ onToggleSelect: () => onToggleId(id),
143
+ scale: cardScale,
144
+ })}
145
+ </Box>
146
+ );
147
+ })}
148
+ </Box>
149
+ );
150
+ }
151
+
152
+ /* ── Body (list) ─────────────────────────────────────────────────────────── */
153
+
154
+ interface ListBodyProps<T extends Record<string, unknown>> {
155
+ rows: T[];
156
+ renderListRow: (row: T, selection: DataViewCardSelection) => React.ReactNode;
157
+ getRowId: (row: T) => string | number;
158
+ selectedIds: Set<string | number>;
159
+ onToggleId: (id: string | number) => void;
160
+ /** Gap between rows, from the density preference. */
161
+ rowGap: number;
162
+ dataTestId?: string;
163
+ emptyState?: React.ReactNode;
164
+ }
165
+
166
+ /**
167
+ * The "Lista" layout: one FULL-WIDTH row per record, rendered by the entity — a
168
+ * marker, a title, a subtitle and a value on the right is the shape it was
169
+ * designed for (FUT-733).
170
+ *
171
+ * It sits between the table and the cards rather than replacing either: the
172
+ * table is for comparing many columns, the cards are for browsing, and the list
173
+ * is for scanning a queue on a narrow screen. Selection is the SAME model as
174
+ * both — each row receives its selection state, so it can drive its own
175
+ * checkbox and the bulk menu behaves identically in every layout.
176
+ *
177
+ * `scale` is handed over as 1 rather than the zoom multiplier, and the zoom
178
+ * slider is hidden in this layout: a full-width row has no card size to
179
+ * multiply. See {@link DataViewsZoomSlider}.
180
+ */
181
+ function ListBody<T extends Record<string, unknown>>({
182
+ rows,
183
+ renderListRow,
184
+ getRowId,
185
+ selectedIds,
186
+ onToggleId,
187
+ rowGap,
188
+ dataTestId,
189
+ emptyState,
190
+ }: ListBodyProps<T>): React.JSX.Element {
191
+ if (rows.length === 0) {
192
+ return <Box sx={{ mt: 1.5 }}>{emptyState}</Box>;
193
+ }
194
+ return (
195
+ <Box
196
+ sx={{ mt: 1.5, display: "flex", flexDirection: "column", gap: rowGap }}
197
+ data-testid={dataTestId ? `${dataTestId}-list` : "data-views-list"}
198
+ >
199
+ {rows.map((row) => {
200
+ const id = getRowId(row);
201
+ return (
202
+ <Box key={id}>
203
+ {renderListRow(row, {
204
+ selected: selectedIds.has(id),
205
+ onToggleSelect: () => onToggleId(id),
206
+ scale: 1,
207
+ })}
208
+ </Box>
209
+ );
210
+ })}
211
+ </Box>
212
+ );
213
+ }
214
+
215
+ /* ── Body selector (board vs list vs cards vs table, from context) ───────── */
216
+
217
+ interface GridMainProps<T extends Record<string, unknown>> {
218
+ c: DataViewsController<T>;
219
+ getRowId: (row: T) => string | number;
220
+ renderCard?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
221
+ /** Opt-in "Lista" layout — one full-width row per record. */
222
+ renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
223
+ /** Opt-in "Quadro" (board) layout — needs `renderCard`, since it reuses the card. */
224
+ board?: BoardConfig<T>;
225
+ dataTestId?: string;
226
+ emptyState?: React.ReactNode;
227
+ testIdPrefix: string;
228
+ }
229
+
230
+ /**
231
+ * The headerless layouts, each preceded by its own select-all.
232
+ *
233
+ * The strip is rendered HERE rather than inside each body so all three agree on
234
+ * where it sits and what it says — and so the table, which already has one in
235
+ * its `<thead>`, never gets a second.
236
+ */
237
+ function Headerless<T extends Record<string, unknown>>({
238
+ c,
239
+ getRowId,
240
+ testIdPrefix,
241
+ children,
242
+ }: {
243
+ c: DataViewsController<T>;
244
+ getRowId: (row: T) => string | number;
245
+ testIdPrefix: string;
246
+ children: React.ReactNode;
247
+ }): React.JSX.Element {
248
+ return (
249
+ <>
250
+ <SelectAllStrip
251
+ rows={c.matched}
252
+ getRowId={getRowId}
253
+ selectedIds={c.selectedIds}
254
+ onChange={(next) => c.setSelectedIds(next)}
255
+ testIdPrefix={testIdPrefix}
256
+ />
257
+ {children}
258
+ </>
259
+ );
260
+ }
261
+
262
+ /** Picks the body from the layout context: board, list, cards, or the dense grid. */
263
+ export function GridMain<T extends Record<string, unknown>>({
264
+ c,
265
+ getRowId,
266
+ renderCard,
267
+ renderListRow,
268
+ board,
269
+ dataTestId,
270
+ emptyState,
271
+ testIdPrefix,
272
+ }: GridMainProps<T>): React.JSX.Element {
273
+ const { layout, zoom, density } = useDataViewsLayout();
274
+ if (layout === "list" && renderListRow) {
275
+ return (
276
+ <Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
277
+ <ListBody
278
+ rows={c.matched}
279
+ renderListRow={renderListRow}
280
+ getRowId={getRowId}
281
+ selectedIds={c.selectedIds}
282
+ onToggleId={c.toggleId}
283
+ rowGap={DENSITY_ROW_PADDING[density]}
284
+ dataTestId={dataTestId}
285
+ emptyState={emptyState}
286
+ />
287
+ </Headerless>
288
+ );
289
+ }
290
+ if (layout === "board" && board && renderCard) {
291
+ return (
292
+ <Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
293
+ <DataViewsBoard
294
+ rows={c.matched}
295
+ board={board}
296
+ getRowId={getRowId}
297
+ renderCard={renderCard}
298
+ selectedIds={c.selectedIds}
299
+ onToggleId={c.toggleId}
300
+ cardScale={cardScaleForZoom(zoom)}
301
+ dataTestId={dataTestId}
302
+ />
303
+ </Headerless>
304
+ );
305
+ }
306
+ if (layout === "cards" && renderCard) {
307
+ return (
308
+ <Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
309
+ <CardBody
310
+ rows={c.matched}
311
+ renderCard={renderCard}
312
+ getRowId={getRowId}
313
+ selectedIds={c.selectedIds}
314
+ onToggleId={c.toggleId}
315
+ minCardWidth={cardMinWidthForZoom(zoom)}
316
+ maxColumns={DENSITY_CARD_COLUMNS[density]}
317
+ cardScale={cardScaleForZoom(zoom)}
318
+ dataTestId={dataTestId}
319
+ emptyState={emptyState}
320
+ />
321
+ </Headerless>
322
+ );
323
+ }
324
+ return (
325
+ <GridBody
326
+ rows={c.matched}
327
+ columns={c.gridColumns}
328
+ getRowId={getRowId}
329
+ selectedIds={[...c.selectedIds]}
330
+ onChangeSelected={(ids) => c.setSelectedIds(new Set(ids))}
331
+ sortBy={c.state.sortBy}
332
+ onChangeSortBy={(next: GridSort[]) => c.patch({ sortBy: next })}
333
+ sortMode={c.serverMode ? "server" : "client"}
334
+ rowPadding={DENSITY_ROW_PADDING[density]}
335
+ dataTestId={dataTestId}
336
+ emptyState={emptyState}
337
+ />
338
+ );
339
+ }
340
+