@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.
- package/package.json +4 -4
- package/src/components/data-display/DataViews/DataViewsBoard.tsx +327 -0
- package/src/components/data-display/DataViews/DataViewsGrid.tsx +80 -77
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +178 -230
- package/src/components/data-display/DataViews/DeleteViewDialog.tsx +83 -0
- package/src/components/data-display/DataViews/SaveViewModal.tsx +140 -52
- package/src/components/data-display/DataViews/data-views-columns-tab.tsx +240 -0
- package/src/components/data-display/DataViews/data-views-columns.ts +129 -0
- package/src/components/data-display/DataViews/data-views-dirty.ts +62 -0
- package/src/components/data-display/DataViews/data-views-display-panel.tsx +328 -0
- package/src/components/data-display/DataViews/data-views-display-tabs.tsx +328 -0
- package/src/components/data-display/DataViews/data-views-empty.tsx +91 -0
- package/src/components/data-display/DataViews/data-views-export.tsx +260 -0
- package/src/components/data-display/DataViews/data-views-glyphs.tsx +103 -0
- package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +340 -0
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +220 -242
- package/src/components/data-display/DataViews/data-views-inline-bar.tsx +187 -81
- package/src/components/data-display/DataViews/data-views-layout-context.tsx +215 -79
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +300 -0
- package/src/components/data-display/DataViews/data-views-overflow.ts +302 -0
- package/src/components/data-display/DataViews/data-views-preview.ts +11 -3
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +162 -92
- package/src/components/data-display/DataViews/data-views-range-values.ts +69 -0
- package/src/components/data-display/DataViews/data-views-scopes.tsx +356 -0
- package/src/components/data-display/DataViews/data-views-search.tsx +156 -0
- package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +105 -0
- package/src/components/data-display/DataViews/data-views-selection.ts +50 -0
- package/src/components/data-display/DataViews/data-views-server-query.ts +75 -0
- package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +87 -0
- package/src/components/data-display/DataViews/data-views-table-parts.tsx +5 -0
- package/src/components/data-display/DataViews/data-views-toolbar.tsx +63 -48
- package/src/components/data-display/DataViews/data-views-types.ts +88 -9
- package/src/components/data-display/DataViews/data-views-view-nav.tsx +378 -0
- package/src/components/data-display/DataViews/index.ts +14 -1
- package/src/components/data-display/DataViews/use-data-views-state.ts +158 -134
- package/src/components/data-display/DataViews/use-saved-views-controller.ts +171 -0
- package/src/components/layout/ContentToolbar/ContentToolbar.tsx +86 -30
- package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +25 -0
- package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +6 -1
- package/src/components/utility/InstallPrompt/InstallPrompt.earlyCapture.ts +117 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.md +52 -4
- package/src/components/utility/InstallPrompt/InstallPrompt.types.ts +19 -0
- package/src/components/utility/InstallPrompt/index.ts +8 -0
- package/src/components/utility/InstallPrompt/usePwaInstall.ts +39 -11
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import FilterAltOffRoundedIcon from "@mui/icons-material/FilterAltOffRounded";
|
|
4
|
+
import InboxRoundedIcon from "@mui/icons-material/InboxRounded";
|
|
5
|
+
|
|
6
|
+
import { Button } from "../../form/Button";
|
|
7
|
+
import { Box } from "../../../mui/Box";
|
|
8
|
+
import { Text } from "../../typography/Text";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* TWO EMPTY STATES, NOT ONE.
|
|
12
|
+
*
|
|
13
|
+
* "Nenhum pagamento registrado" under an active filter is a lie, and it is the
|
|
14
|
+
* expensive kind: the operator reads it as "this store has no payments" and
|
|
15
|
+
* goes looking for the bug somewhere else. The page's own empty copy answers
|
|
16
|
+
* "there is nothing here yet"; it cannot answer "nothing matches what you
|
|
17
|
+
* typed", because it does not know what was typed.
|
|
18
|
+
*
|
|
19
|
+
* So the grid renders the FILTERED variant itself — it is the only party that
|
|
20
|
+
* knows a filter is applied — and defers to the host's `emptyState` only when
|
|
21
|
+
* the list is genuinely empty. The filtered one carries the way out, which is
|
|
22
|
+
* the whole point: the fix is one click, and it is right there.
|
|
23
|
+
*/
|
|
24
|
+
export function DataViewsEmpty({
|
|
25
|
+
filtered,
|
|
26
|
+
onClearFilters,
|
|
27
|
+
emptyState,
|
|
28
|
+
testIdPrefix,
|
|
29
|
+
}: {
|
|
30
|
+
/** Is anything narrowing the list right now (search, pills or ranges)? */
|
|
31
|
+
filtered: boolean;
|
|
32
|
+
onClearFilters: () => void;
|
|
33
|
+
/** The page's own copy for a genuinely empty list. */
|
|
34
|
+
emptyState?: React.ReactNode;
|
|
35
|
+
testIdPrefix: string;
|
|
36
|
+
}): React.JSX.Element {
|
|
37
|
+
const Icon = filtered ? FilterAltOffRoundedIcon : InboxRoundedIcon;
|
|
38
|
+
return (
|
|
39
|
+
<Box
|
|
40
|
+
data-testid={`${testIdPrefix}-empty${filtered ? "-filtered" : ""}`}
|
|
41
|
+
sx={{
|
|
42
|
+
display: "flex",
|
|
43
|
+
flexDirection: "column",
|
|
44
|
+
alignItems: "center",
|
|
45
|
+
textAlign: "center",
|
|
46
|
+
py: 8,
|
|
47
|
+
px: 2,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<Box
|
|
51
|
+
sx={{
|
|
52
|
+
display: "inline-flex",
|
|
53
|
+
p: 2,
|
|
54
|
+
mb: 1.5,
|
|
55
|
+
borderRadius: "50%",
|
|
56
|
+
bgcolor: "action.hover",
|
|
57
|
+
color: "text.disabled",
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<Icon />
|
|
61
|
+
</Box>
|
|
62
|
+
{filtered ? (
|
|
63
|
+
<>
|
|
64
|
+
<Text variant="body" as="p">
|
|
65
|
+
<Box component="span" sx={{ fontWeight: 600 }}>
|
|
66
|
+
Nenhum resultado para esses filtros
|
|
67
|
+
</Box>
|
|
68
|
+
</Text>
|
|
69
|
+
<Text variant="caption" as="p">
|
|
70
|
+
<Box component="span" sx={{ display: "block", mt: 0.5, mb: 2, color: "text.secondary", maxWidth: 380 }}>
|
|
71
|
+
Ajuste ou remova os filtros.
|
|
72
|
+
</Box>
|
|
73
|
+
</Text>
|
|
74
|
+
<Button
|
|
75
|
+
variant="outline"
|
|
76
|
+
size="sm"
|
|
77
|
+
color="secondary"
|
|
78
|
+
onClick={onClearFilters}
|
|
79
|
+
dataTestId={`${testIdPrefix}-empty-clear`}
|
|
80
|
+
>
|
|
81
|
+
Limpar filtros
|
|
82
|
+
</Button>
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
// The page's own words for "nothing here yet" — it knows what the
|
|
86
|
+
// entity is and how one comes to exist; this component does not.
|
|
87
|
+
emptyState
|
|
88
|
+
)}
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import DownloadRoundedIcon from "@mui/icons-material/DownloadRounded";
|
|
4
|
+
import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
|
|
5
|
+
import { CircularProgress, Popover } from "@mui/material";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
|
|
8
|
+
import { Button } from "../../form/Button";
|
|
9
|
+
import { Box } from "../../../mui/Box";
|
|
10
|
+
import { Text } from "../../typography/Text";
|
|
11
|
+
|
|
12
|
+
import type { DataViewQuery } from "./data-views-types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* EXPORT — the current QUERY, never the rendered page.
|
|
16
|
+
*
|
|
17
|
+
* Exporting what happens to be on screen is the single most common bug in an
|
|
18
|
+
* admin table: the operator filtered 214 records, sees 25, exports, and gets
|
|
19
|
+
* 25 — with nothing anywhere saying so. So this control does not touch the
|
|
20
|
+
* loaded rows at all. It hands the host the same `DataViewQuery` the grid is
|
|
21
|
+
* showing, unpaginated, and the HOST re-queries the backend — the same
|
|
22
|
+
* division as everywhere else here: the component emits, the host fetches.
|
|
23
|
+
*
|
|
24
|
+
* The menu states the number it is about to export before the click, so the
|
|
25
|
+
* two possible answers (the selection, or the whole filtered set) are never a
|
|
26
|
+
* guess.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** What a host can be asked to produce. */
|
|
30
|
+
export type DataViewExportFormat = "xlsx" | "csv" | "json";
|
|
31
|
+
|
|
32
|
+
/** The request handed to the host when the operator picks a format. */
|
|
33
|
+
export interface DataViewExportRequest {
|
|
34
|
+
format: DataViewExportFormat;
|
|
35
|
+
/**
|
|
36
|
+
* The live query with `page: 1` and `pageSize` set to the whole matched
|
|
37
|
+
* total — i.e. "everything this filter selects", not this page.
|
|
38
|
+
*/
|
|
39
|
+
query: DataViewQuery;
|
|
40
|
+
/**
|
|
41
|
+
* The selected row ids, when the operator has a selection. The host filters
|
|
42
|
+
* the re-queried rows to these; absent means the whole filtered set.
|
|
43
|
+
*/
|
|
44
|
+
selectedIds?: Array<string | number>;
|
|
45
|
+
/** The visible columns, in the operator's current order. */
|
|
46
|
+
columns: { id: string; label: string }[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Injected export. Absent ⇒ no Exportar control, exactly like `renderCard`. */
|
|
50
|
+
export interface DataViewExport {
|
|
51
|
+
/** Runs the export. Awaited, so the trigger can show it is working. */
|
|
52
|
+
onExport: (request: DataViewExportRequest) => Promise<void> | void;
|
|
53
|
+
/** Formats to offer, in menu order. Defaults to all three. */
|
|
54
|
+
formats?: DataViewExportFormat[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One line per format: what it is, and when to reach for it. */
|
|
58
|
+
const FORMAT_COPY: Record<DataViewExportFormat, { label: string; hint: string }> = {
|
|
59
|
+
xlsx: { label: "Excel (.xlsx)", hint: "Abre direto, com as colunas dimensionadas" },
|
|
60
|
+
csv: { label: "CSV (.csv)", hint: "Para importar em outro sistema" },
|
|
61
|
+
json: { label: "JSON (.json)", hint: "Para integrações" },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const ALL_FORMATS: DataViewExportFormat[] = ["xlsx", "csv", "json"];
|
|
65
|
+
|
|
66
|
+
interface ExportMenuProps {
|
|
67
|
+
config: DataViewExport;
|
|
68
|
+
/** The live query — copied and unpaginated before it reaches the host. */
|
|
69
|
+
query: DataViewQuery;
|
|
70
|
+
/** The unpaginated total, so the label can state what "tudo" means. */
|
|
71
|
+
totalCount: number;
|
|
72
|
+
selectedIds: Array<string | number>;
|
|
73
|
+
columns: { id: string; label: string }[];
|
|
74
|
+
testIdPrefix: string;
|
|
75
|
+
/** Render icon-only (measured upstream). */
|
|
76
|
+
compact?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** "Exportando 214 itens filtrados" / "3 itens selecionados" — never a guess. */
|
|
80
|
+
function scopeLabel(selected: number, total: number): string {
|
|
81
|
+
if (selected > 0) {
|
|
82
|
+
return `${selected} ${selected === 1 ? "item selecionado" : "itens selecionados"}`;
|
|
83
|
+
}
|
|
84
|
+
return `${total} ${total === 1 ? "item filtrado" : "itens filtrados"}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The trigger. Shows a spinner while the host's export is still running. */
|
|
88
|
+
function ExportTrigger({
|
|
89
|
+
busy,
|
|
90
|
+
onOpen,
|
|
91
|
+
testIdPrefix,
|
|
92
|
+
compact,
|
|
93
|
+
}: {
|
|
94
|
+
busy: boolean;
|
|
95
|
+
onOpen: (anchor: HTMLElement) => void;
|
|
96
|
+
testIdPrefix: string;
|
|
97
|
+
/** Icon only — step 2 of the toolbar's degradation ladder. */
|
|
98
|
+
compact?: boolean;
|
|
99
|
+
}): React.JSX.Element {
|
|
100
|
+
return (
|
|
101
|
+
<Button
|
|
102
|
+
variant="outline"
|
|
103
|
+
size="sm"
|
|
104
|
+
color="neutral"
|
|
105
|
+
disabled={busy}
|
|
106
|
+
onClick={(event) => onOpen(event.currentTarget as HTMLElement)}
|
|
107
|
+
dataTestId={`${testIdPrefix}-export-trigger`}
|
|
108
|
+
aria-label="Exportar"
|
|
109
|
+
// The label is gone, so the tooltip is the only thing naming the control.
|
|
110
|
+
title={compact ? "Exportar" : undefined}
|
|
111
|
+
>
|
|
112
|
+
<Box component="span" sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}>
|
|
113
|
+
{busy ? <CircularProgress size={14} /> : <DownloadRoundedIcon fontSize="small" />}
|
|
114
|
+
{!compact && (
|
|
115
|
+
<Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
|
|
116
|
+
Exportar
|
|
117
|
+
</Box>
|
|
118
|
+
)}
|
|
119
|
+
<KeyboardArrowDownRoundedIcon fontSize="small" />
|
|
120
|
+
</Box>
|
|
121
|
+
</Button>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One format row: what it produces, and when to reach for it. */
|
|
126
|
+
function FormatRow({
|
|
127
|
+
format,
|
|
128
|
+
onPick,
|
|
129
|
+
testIdPrefix,
|
|
130
|
+
}: {
|
|
131
|
+
format: DataViewExportFormat;
|
|
132
|
+
onPick: () => void;
|
|
133
|
+
testIdPrefix: string;
|
|
134
|
+
}): React.JSX.Element {
|
|
135
|
+
return (
|
|
136
|
+
<Box
|
|
137
|
+
component="button"
|
|
138
|
+
type="button"
|
|
139
|
+
onClick={onPick}
|
|
140
|
+
data-testid={`${testIdPrefix}-export-${format}`}
|
|
141
|
+
sx={{
|
|
142
|
+
display: "flex",
|
|
143
|
+
flexDirection: "column",
|
|
144
|
+
alignItems: "flex-start",
|
|
145
|
+
width: "100%",
|
|
146
|
+
px: 1.5,
|
|
147
|
+
py: 1,
|
|
148
|
+
border: 0,
|
|
149
|
+
background: "none",
|
|
150
|
+
cursor: "pointer",
|
|
151
|
+
font: "inherit",
|
|
152
|
+
textAlign: "left",
|
|
153
|
+
"&:hover": { bgcolor: "action.hover" },
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<Box component="span" sx={{ fontSize: "0.8125rem" }}>
|
|
157
|
+
{FORMAT_COPY[format].label}
|
|
158
|
+
</Box>
|
|
159
|
+
<Box component="span" sx={{ fontSize: "0.6875rem", color: "text.disabled" }}>
|
|
160
|
+
{FORMAT_COPY[format].hint}
|
|
161
|
+
</Box>
|
|
162
|
+
</Box>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The panel: what is about to be exported, then the formats. */
|
|
167
|
+
function ExportPanel({
|
|
168
|
+
formats,
|
|
169
|
+
selectedCount,
|
|
170
|
+
totalCount,
|
|
171
|
+
columnCount,
|
|
172
|
+
onPick,
|
|
173
|
+
testIdPrefix,
|
|
174
|
+
}: {
|
|
175
|
+
formats: DataViewExportFormat[];
|
|
176
|
+
selectedCount: number;
|
|
177
|
+
totalCount: number;
|
|
178
|
+
columnCount: number;
|
|
179
|
+
onPick: (format: DataViewExportFormat) => void;
|
|
180
|
+
testIdPrefix: string;
|
|
181
|
+
}): React.JSX.Element {
|
|
182
|
+
return (
|
|
183
|
+
<Box data-testid={`${testIdPrefix}-export-panel`}>
|
|
184
|
+
<Box sx={{ px: 1.5, pt: 1, pb: 1, borderBottom: 1, borderColor: "divider" }}>
|
|
185
|
+
<Text variant="caption" as="p">
|
|
186
|
+
<Box component="span" data-testid={`${testIdPrefix}-export-scope`}>
|
|
187
|
+
Exportando {scopeLabel(selectedCount, totalCount)}
|
|
188
|
+
</Box>
|
|
189
|
+
</Text>
|
|
190
|
+
<Text variant="caption" as="p">
|
|
191
|
+
<Box component="span" sx={{ color: "text.disabled" }}>
|
|
192
|
+
{columnCount} colunas visíveis, na ordem atual
|
|
193
|
+
</Box>
|
|
194
|
+
</Text>
|
|
195
|
+
</Box>
|
|
196
|
+
{formats.map((format) => (
|
|
197
|
+
<FormatRow
|
|
198
|
+
key={format}
|
|
199
|
+
format={format}
|
|
200
|
+
onPick={() => onPick(format)}
|
|
201
|
+
testIdPrefix={testIdPrefix}
|
|
202
|
+
/>
|
|
203
|
+
))}
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The toolbar's Exportar control. Renders nothing when no host wired it. */
|
|
209
|
+
export function DataViewsExportMenu({
|
|
210
|
+
config,
|
|
211
|
+
query,
|
|
212
|
+
totalCount,
|
|
213
|
+
selectedIds,
|
|
214
|
+
columns,
|
|
215
|
+
testIdPrefix,
|
|
216
|
+
compact,
|
|
217
|
+
}: ExportMenuProps): React.JSX.Element {
|
|
218
|
+
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
|
219
|
+
const [busy, setBusy] = useState(false);
|
|
220
|
+
|
|
221
|
+
const run = async (format: DataViewExportFormat): Promise<void> => {
|
|
222
|
+
setAnchor(null);
|
|
223
|
+
setBusy(true);
|
|
224
|
+
try {
|
|
225
|
+
await config.onExport({
|
|
226
|
+
format,
|
|
227
|
+
// UNPAGINATED: page 1 of a page as large as the whole result. The host
|
|
228
|
+
// re-queries with this; nothing here reads the loaded rows.
|
|
229
|
+
query: { ...query, page: 1, pageSize: Math.max(1, totalCount) },
|
|
230
|
+
selectedIds: selectedIds.length > 0 ? [...selectedIds] : undefined,
|
|
231
|
+
columns,
|
|
232
|
+
});
|
|
233
|
+
} finally {
|
|
234
|
+
setBusy(false);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<>
|
|
240
|
+
<ExportTrigger busy={busy} onOpen={setAnchor} testIdPrefix={testIdPrefix} compact={compact} />
|
|
241
|
+
<Popover
|
|
242
|
+
open={Boolean(anchor)}
|
|
243
|
+
anchorEl={anchor}
|
|
244
|
+
onClose={() => setAnchor(null)}
|
|
245
|
+
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
|
246
|
+
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
|
247
|
+
slotProps={{ paper: { sx: { width: 260, maxWidth: "calc(100vw - 32px)" } } }}
|
|
248
|
+
>
|
|
249
|
+
<ExportPanel
|
|
250
|
+
formats={config.formats ?? ALL_FORMATS}
|
|
251
|
+
selectedCount={selectedIds.length}
|
|
252
|
+
totalCount={totalCount}
|
|
253
|
+
columnCount={columns.length}
|
|
254
|
+
onPick={(format) => void run(format)}
|
|
255
|
+
testIdPrefix={testIdPrefix}
|
|
256
|
+
/>
|
|
257
|
+
</Popover>
|
|
258
|
+
</>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Box } from "../../../mui/Box";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Miniature previews of what a layout or a density setting DOES.
|
|
7
|
+
*
|
|
8
|
+
* "Baixa / Média / Alta" tells an operator nothing until they try it, and
|
|
9
|
+
* "Grade" vs "Lista" is only obvious to whoever named them. A glyph that draws
|
|
10
|
+
* three tight rows, or three loose ones, or a grid of blocks, answers the
|
|
11
|
+
* question before the click — so the picker is read once instead of cycled
|
|
12
|
+
* through.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately CSS boxes rather than icons: the point is to show spacing and
|
|
15
|
+
* proportion, which is exactly what an icon font flattens away.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The glyph's own footprint, so every tile in a picker lines up. */
|
|
19
|
+
const FRAME = { width: 32, height: 24, display: "flex" } as const;
|
|
20
|
+
|
|
21
|
+
/** Active tiles use the accent; the rest stay muted so the choice reads at a glance. */
|
|
22
|
+
function barColor(active: boolean): string {
|
|
23
|
+
return active ? "primary.main" : "action.disabled";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Three rows at a given gap — the density preview for the table and list. */
|
|
27
|
+
export function RowsGlyph({ gap, active }: { gap: number; active: boolean }): React.JSX.Element {
|
|
28
|
+
return (
|
|
29
|
+
<Box sx={{ ...FRAME, flexDirection: "column", justifyContent: "center", gap: `${gap}px` }}>
|
|
30
|
+
{[0, 1, 2].map((index) => (
|
|
31
|
+
<Box key={index} sx={{ height: 2, width: "100%", borderRadius: 1, bgcolor: barColor(active) }} />
|
|
32
|
+
))}
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A dense block grid — the table layout. */
|
|
38
|
+
export function TableGlyph({ active }: { active: boolean }): React.JSX.Element {
|
|
39
|
+
return (
|
|
40
|
+
<Box sx={{ ...FRAME, display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "2px" }}>
|
|
41
|
+
{Array.from({ length: 9 }).map((_, index) => (
|
|
42
|
+
<Box
|
|
43
|
+
key={index}
|
|
44
|
+
sx={{ borderRadius: 0.5, bgcolor: barColor(active), opacity: index < 3 ? 1 : 0.55 }}
|
|
45
|
+
/>
|
|
46
|
+
))}
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Full-width rows each carrying a leading mark — the list layout. */
|
|
52
|
+
export function ListGlyph({ active }: { active: boolean }): React.JSX.Element {
|
|
53
|
+
return (
|
|
54
|
+
<Box sx={{ ...FRAME, flexDirection: "column", justifyContent: "center", gap: "4px" }}>
|
|
55
|
+
{[0, 1, 2].map((index) => (
|
|
56
|
+
<Box
|
|
57
|
+
key={index}
|
|
58
|
+
sx={{
|
|
59
|
+
height: 5,
|
|
60
|
+
width: "100%",
|
|
61
|
+
borderRadius: 0.5,
|
|
62
|
+
bgcolor: barColor(active),
|
|
63
|
+
opacity: 0.7,
|
|
64
|
+
display: "flex",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<Box sx={{ ml: "2px", height: 3, width: 3, borderRadius: "50%", bgcolor: "background.paper" }} />
|
|
69
|
+
</Box>
|
|
70
|
+
))}
|
|
71
|
+
</Box>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A grid of blocks — the cards layout, and ALSO the cards density preview: `n`
|
|
77
|
+
* is how many fit per row, so "Muitos / Médio / Poucos" is shown rather than
|
|
78
|
+
* described.
|
|
79
|
+
*/
|
|
80
|
+
export function GridGlyph({ n = 3, active }: { n?: number; active: boolean }): React.JSX.Element {
|
|
81
|
+
return (
|
|
82
|
+
<Box sx={{ ...FRAME, display: "grid", gridTemplateColumns: `repeat(${n}, 1fr)`, gap: "2px" }}>
|
|
83
|
+
{Array.from({ length: n * 2 }).map((_, index) => (
|
|
84
|
+
<Box key={index} sx={{ borderRadius: 0.5, bgcolor: barColor(active) }} />
|
|
85
|
+
))}
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Three columns of decreasing height — the board. */
|
|
91
|
+
export function BoardGlyph({ active }: { active: boolean }): React.JSX.Element {
|
|
92
|
+
return (
|
|
93
|
+
<Box sx={{ ...FRAME, gap: "2px" }}>
|
|
94
|
+
{[3, 2, 1].map((count, column) => (
|
|
95
|
+
<Box key={column} sx={{ flex: 1, display: "flex", flexDirection: "column", gap: "2px" }}>
|
|
96
|
+
{Array.from({ length: count }).map((_, index) => (
|
|
97
|
+
<Box key={index} sx={{ height: 5, borderRadius: 0.5, bgcolor: barColor(active) }} />
|
|
98
|
+
))}
|
|
99
|
+
</Box>
|
|
100
|
+
))}
|
|
101
|
+
</Box>
|
|
102
|
+
);
|
|
103
|
+
}
|