@12-apps/ui 5.0.1 → 5.2.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/components-catalog.md +7 -0
- package/dist/layout/SettingsLayout.js +563 -109
- package/dist/layout/SettingsLayout.js.map +1 -1
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.d.ts +19 -2
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.d.ts.map +1 -1
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.filters.d.ts +6 -0
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.filters.d.ts.map +1 -0
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.items.d.ts +31 -0
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.items.d.ts.map +1 -0
- package/dist/types/components/layout/SettingsLayout/{SettingsLayout.parts.d.ts → SettingsLayout.rail.d.ts} +6 -9
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.rail.d.ts.map +1 -0
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.styles.d.ts +35 -0
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.styles.d.ts.map +1 -0
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.types.d.ts +109 -0
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.types.d.ts.map +1 -1
- package/dist/types/components/layout/SettingsLayout/SettingsPanel.d.ts +29 -0
- package/dist/types/components/layout/SettingsLayout/SettingsPanel.d.ts.map +1 -0
- package/dist/types/components/layout/SettingsLayout/SettingsSectionChips.d.ts +26 -0
- package/dist/types/components/layout/SettingsLayout/SettingsSectionChips.d.ts.map +1 -0
- package/dist/types/components/layout/SettingsLayout/SettingsStatusMarker.d.ts +28 -0
- package/dist/types/components/layout/SettingsLayout/SettingsStatusMarker.d.ts.map +1 -0
- package/dist/types/components/layout/SettingsLayout/index.d.ts +5 -1
- package/dist/types/components/layout/SettingsLayout/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/layout/SettingsLayout/SettingsLayout.filters.ts +21 -0
- package/src/components/layout/SettingsLayout/SettingsLayout.items.tsx +228 -0
- package/src/components/layout/SettingsLayout/SettingsLayout.md +93 -1
- package/src/components/layout/SettingsLayout/SettingsLayout.rail.tsx +279 -0
- package/src/components/layout/SettingsLayout/SettingsLayout.styles.ts +64 -0
- package/src/components/layout/SettingsLayout/SettingsLayout.tsx +118 -25
- package/src/components/layout/SettingsLayout/SettingsLayout.types.ts +113 -0
- package/src/components/layout/SettingsLayout/SettingsPanel.tsx +140 -0
- package/src/components/layout/SettingsLayout/SettingsSectionChips.tsx +0 -0
- package/src/components/layout/SettingsLayout/SettingsStatusMarker.tsx +75 -0
- package/src/components/layout/SettingsLayout/index.ts +8 -0
- package/dist/types/components/layout/SettingsLayout/SettingsLayout.parts.d.ts.map +0 -1
- package/src/components/layout/SettingsLayout/SettingsLayout.parts.tsx +0 -358
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
4
|
-
import SearchIcon from '@mui/icons-material/Search';
|
|
5
|
-
import {
|
|
6
|
-
alpha,
|
|
7
|
-
Box,
|
|
8
|
-
InputAdornment,
|
|
9
|
-
List,
|
|
10
|
-
ListItemButton,
|
|
11
|
-
ListItemIcon,
|
|
12
|
-
ListItemText,
|
|
13
|
-
TextField,
|
|
14
|
-
Typography,
|
|
15
|
-
useTheme,
|
|
16
|
-
} from '@mui/material';
|
|
17
|
-
import React from 'react';
|
|
18
|
-
|
|
19
|
-
import type { SettingsLayoutProps, SettingsNavGroup, SettingsNavItem } from './SettingsLayout.types';
|
|
20
|
-
|
|
21
|
-
/** Fixed width of the left navigation rail on `md`+ screens. */
|
|
22
|
-
const RAIL_WIDTH = 300;
|
|
23
|
-
|
|
24
|
-
/** Case-insensitive filter over item label + keywords; drops emptied groups. */
|
|
25
|
-
export function filterGroups(groups: SettingsNavGroup[], query: string): SettingsNavGroup[] {
|
|
26
|
-
const needle = query.trim().toLowerCase();
|
|
27
|
-
if (!needle) return groups;
|
|
28
|
-
const matches = (item: SettingsNavItem): boolean =>
|
|
29
|
-
item.label.toLowerCase().includes(needle) ||
|
|
30
|
-
(item.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(needle));
|
|
31
|
-
return groups
|
|
32
|
-
.map((group) => ({ ...group, items: group.items.filter(matches) }))
|
|
33
|
-
.filter((group) => group.items.length > 0);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Label of the active item, shown in the mobile section-switcher header. */
|
|
37
|
-
export function activeItemLabel(
|
|
38
|
-
groups: SettingsNavGroup[],
|
|
39
|
-
activeItemId?: string,
|
|
40
|
-
): string | undefined {
|
|
41
|
-
return groups.flatMap((group) => group.items).find((item) => item.id === activeItemId)?.label;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface RailItemProps {
|
|
45
|
-
item: SettingsNavItem;
|
|
46
|
-
active: boolean;
|
|
47
|
-
linkComponent?: SettingsLayoutProps['linkComponent'];
|
|
48
|
-
onSelectItem?: (id: string) => void;
|
|
49
|
-
/** Called on activation so the mobile rail can collapse after a choice. */
|
|
50
|
-
onNavigate?: () => void;
|
|
51
|
-
testIdPrefix: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** One rail entry — rendered as a link when it has `href`, else a button. */
|
|
55
|
-
function SettingsRailItem({
|
|
56
|
-
item,
|
|
57
|
-
active,
|
|
58
|
-
linkComponent,
|
|
59
|
-
onSelectItem,
|
|
60
|
-
onNavigate,
|
|
61
|
-
testIdPrefix,
|
|
62
|
-
}: RailItemProps): React.JSX.Element {
|
|
63
|
-
const theme = useTheme();
|
|
64
|
-
const shared = {
|
|
65
|
-
selected: active,
|
|
66
|
-
'aria-current': active ? ('page' as const) : undefined,
|
|
67
|
-
'data-testid': `${testIdPrefix}-item-${item.id}`,
|
|
68
|
-
sx: {
|
|
69
|
-
borderRadius: 1.5,
|
|
70
|
-
mb: 0.25,
|
|
71
|
-
gap: 1.5,
|
|
72
|
-
color: active ? 'primary.main' : 'text.secondary',
|
|
73
|
-
'&.Mui-selected': {
|
|
74
|
-
bgcolor: alpha(theme.palette.primary.main, 0.12),
|
|
75
|
-
'&:hover': { bgcolor: alpha(theme.palette.primary.main, 0.18) },
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
const content = (
|
|
80
|
-
<>
|
|
81
|
-
{item.icon ? (
|
|
82
|
-
<ListItemIcon sx={{ minWidth: 0, color: 'inherit' }}>{item.icon}</ListItemIcon>
|
|
83
|
-
) : null}
|
|
84
|
-
<ListItemText
|
|
85
|
-
primary={item.label}
|
|
86
|
-
primaryTypographyProps={{ variant: 'body2', noWrap: true, fontWeight: active ? 600 : 500 }}
|
|
87
|
-
/>
|
|
88
|
-
</>
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
if (item.href && linkComponent) {
|
|
92
|
-
const LinkComponent = linkComponent;
|
|
93
|
-
return (
|
|
94
|
-
<ListItemButton component={LinkComponent} href={item.href} onClick={onNavigate} {...shared}>
|
|
95
|
-
{content}
|
|
96
|
-
</ListItemButton>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
return (
|
|
100
|
-
<ListItemButton
|
|
101
|
-
onClick={() => {
|
|
102
|
-
onSelectItem?.(item.id);
|
|
103
|
-
onNavigate?.();
|
|
104
|
-
}}
|
|
105
|
-
{...shared}
|
|
106
|
-
>
|
|
107
|
-
{content}
|
|
108
|
-
</ListItemButton>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
interface RailGroupProps {
|
|
113
|
-
group: SettingsNavGroup;
|
|
114
|
-
activeItemId?: string;
|
|
115
|
-
linkComponent?: SettingsLayoutProps['linkComponent'];
|
|
116
|
-
onSelectItem?: (id: string) => void;
|
|
117
|
-
onNavigate?: () => void;
|
|
118
|
-
testIdPrefix: string;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** A category: header + optional description + its items. */
|
|
122
|
-
function SettingsRailGroup({
|
|
123
|
-
group,
|
|
124
|
-
activeItemId,
|
|
125
|
-
linkComponent,
|
|
126
|
-
onSelectItem,
|
|
127
|
-
onNavigate,
|
|
128
|
-
testIdPrefix,
|
|
129
|
-
}: RailGroupProps): React.JSX.Element {
|
|
130
|
-
return (
|
|
131
|
-
<Box data-testid={`${testIdPrefix}-group-${group.id}`} sx={{ mb: 2 }}>
|
|
132
|
-
<Typography
|
|
133
|
-
variant="caption"
|
|
134
|
-
sx={{
|
|
135
|
-
display: 'block',
|
|
136
|
-
px: 1,
|
|
137
|
-
color: 'text.secondary',
|
|
138
|
-
fontWeight: 600,
|
|
139
|
-
textTransform: 'uppercase',
|
|
140
|
-
letterSpacing: '0.06em',
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
{group.label}
|
|
144
|
-
</Typography>
|
|
145
|
-
{group.description ? (
|
|
146
|
-
<Typography variant="caption" component="p" sx={{ px: 1, mt: 0.25, color: 'text.disabled' }}>
|
|
147
|
-
{group.description}
|
|
148
|
-
</Typography>
|
|
149
|
-
) : null}
|
|
150
|
-
<List disablePadding sx={{ mt: 0.5 }}>
|
|
151
|
-
{group.items.map((item) => (
|
|
152
|
-
<SettingsRailItem
|
|
153
|
-
key={item.id}
|
|
154
|
-
item={item}
|
|
155
|
-
active={item.id === activeItemId}
|
|
156
|
-
linkComponent={linkComponent}
|
|
157
|
-
onSelectItem={onSelectItem}
|
|
158
|
-
onNavigate={onNavigate}
|
|
159
|
-
testIdPrefix={testIdPrefix}
|
|
160
|
-
/>
|
|
161
|
-
))}
|
|
162
|
-
</List>
|
|
163
|
-
</Box>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
interface RailToggleProps {
|
|
168
|
-
title?: React.ReactNode;
|
|
169
|
-
activeLabel?: string;
|
|
170
|
-
open: boolean;
|
|
171
|
-
onToggle: () => void;
|
|
172
|
-
testIdPrefix: string;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Mobile-only collapse control: shows the current section (a compact switcher)
|
|
177
|
-
* so the panel content stays at the top of the screen. Hidden on `md`+.
|
|
178
|
-
*/
|
|
179
|
-
function SettingsRailToggle({
|
|
180
|
-
title,
|
|
181
|
-
activeLabel,
|
|
182
|
-
open,
|
|
183
|
-
onToggle,
|
|
184
|
-
testIdPrefix,
|
|
185
|
-
}: RailToggleProps): React.JSX.Element {
|
|
186
|
-
return (
|
|
187
|
-
<Box
|
|
188
|
-
component="button"
|
|
189
|
-
type="button"
|
|
190
|
-
onClick={onToggle}
|
|
191
|
-
aria-expanded={open}
|
|
192
|
-
aria-label={typeof title === 'string' ? title : 'Settings'}
|
|
193
|
-
data-testid={`${testIdPrefix}-toggle`}
|
|
194
|
-
sx={{
|
|
195
|
-
display: { xs: 'flex', md: 'none' },
|
|
196
|
-
alignItems: 'center',
|
|
197
|
-
justifyContent: 'space-between',
|
|
198
|
-
gap: 1,
|
|
199
|
-
width: '100%',
|
|
200
|
-
px: 1.5,
|
|
201
|
-
py: 1,
|
|
202
|
-
mb: 1,
|
|
203
|
-
textAlign: 'left',
|
|
204
|
-
cursor: 'pointer',
|
|
205
|
-
color: 'text.primary',
|
|
206
|
-
background: 'transparent',
|
|
207
|
-
border: (theme) => `1px solid ${theme.palette.divider}`,
|
|
208
|
-
borderRadius: 1.5,
|
|
209
|
-
}}
|
|
210
|
-
>
|
|
211
|
-
<Box sx={{ minWidth: 0 }}>
|
|
212
|
-
{title ? (
|
|
213
|
-
<Typography variant="caption" sx={{ display: 'block', color: 'text.secondary' }}>
|
|
214
|
-
{title}
|
|
215
|
-
</Typography>
|
|
216
|
-
) : null}
|
|
217
|
-
<Typography variant="body2" noWrap sx={{ fontWeight: 600 }}>
|
|
218
|
-
{activeLabel ?? ''}
|
|
219
|
-
</Typography>
|
|
220
|
-
</Box>
|
|
221
|
-
<ExpandMoreIcon
|
|
222
|
-
fontSize="small"
|
|
223
|
-
sx={{
|
|
224
|
-
flex: '0 0 auto',
|
|
225
|
-
color: 'text.secondary',
|
|
226
|
-
transition: 'transform 150ms ease',
|
|
227
|
-
transform: open ? 'rotate(180deg)' : 'none',
|
|
228
|
-
}}
|
|
229
|
-
/>
|
|
230
|
-
</Box>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
interface RailBodyProps {
|
|
235
|
-
navOpen: boolean;
|
|
236
|
-
query: string;
|
|
237
|
-
onQueryChange: (value: string) => void;
|
|
238
|
-
searchPlaceholder: string;
|
|
239
|
-
emptySearchLabel: React.ReactNode;
|
|
240
|
-
filteredGroups: SettingsNavGroup[];
|
|
241
|
-
activeItemId?: string;
|
|
242
|
-
linkComponent?: SettingsLayoutProps['linkComponent'];
|
|
243
|
-
onSelectItem?: (id: string) => void;
|
|
244
|
-
onNavigate?: () => void;
|
|
245
|
-
testIdPrefix: string;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** The searchable rail body (search + groups); hidden on mobile until expanded. */
|
|
249
|
-
function SettingsRailBody({
|
|
250
|
-
navOpen,
|
|
251
|
-
query,
|
|
252
|
-
onQueryChange,
|
|
253
|
-
searchPlaceholder,
|
|
254
|
-
emptySearchLabel,
|
|
255
|
-
filteredGroups,
|
|
256
|
-
activeItemId,
|
|
257
|
-
linkComponent,
|
|
258
|
-
onSelectItem,
|
|
259
|
-
onNavigate,
|
|
260
|
-
testIdPrefix,
|
|
261
|
-
}: RailBodyProps): React.JSX.Element {
|
|
262
|
-
return (
|
|
263
|
-
<Box
|
|
264
|
-
data-testid={`${testIdPrefix}-rail-body`}
|
|
265
|
-
sx={{ display: { xs: navOpen ? 'block' : 'none', md: 'block' } }}
|
|
266
|
-
>
|
|
267
|
-
<TextField
|
|
268
|
-
value={query}
|
|
269
|
-
onChange={(event) => onQueryChange(event.target.value)}
|
|
270
|
-
placeholder={searchPlaceholder}
|
|
271
|
-
size="small"
|
|
272
|
-
fullWidth
|
|
273
|
-
aria-label={searchPlaceholder}
|
|
274
|
-
data-testid={`${testIdPrefix}-search`}
|
|
275
|
-
InputProps={{
|
|
276
|
-
startAdornment: (
|
|
277
|
-
<InputAdornment position="start">
|
|
278
|
-
<SearchIcon fontSize="small" sx={{ color: 'text.secondary' }} />
|
|
279
|
-
</InputAdornment>
|
|
280
|
-
),
|
|
281
|
-
}}
|
|
282
|
-
sx={{ mb: 2 }}
|
|
283
|
-
/>
|
|
284
|
-
{filteredGroups.length === 0 ? (
|
|
285
|
-
<Typography
|
|
286
|
-
variant="body2"
|
|
287
|
-
color="text.secondary"
|
|
288
|
-
data-testid={`${testIdPrefix}-empty`}
|
|
289
|
-
sx={{ px: 1, py: 2 }}
|
|
290
|
-
>
|
|
291
|
-
{emptySearchLabel}
|
|
292
|
-
</Typography>
|
|
293
|
-
) : (
|
|
294
|
-
filteredGroups.map((group) => (
|
|
295
|
-
<SettingsRailGroup
|
|
296
|
-
key={group.id}
|
|
297
|
-
group={group}
|
|
298
|
-
activeItemId={activeItemId}
|
|
299
|
-
linkComponent={linkComponent}
|
|
300
|
-
onSelectItem={onSelectItem}
|
|
301
|
-
onNavigate={onNavigate}
|
|
302
|
-
testIdPrefix={testIdPrefix}
|
|
303
|
-
/>
|
|
304
|
-
))
|
|
305
|
-
)}
|
|
306
|
-
</Box>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Props for {@link SettingsRail}: the rail body's inputs plus the mobile chrome. */
|
|
311
|
-
export type SettingsRailProps = RailBodyProps & {
|
|
312
|
-
title?: React.ReactNode;
|
|
313
|
-
activeLabel?: string;
|
|
314
|
-
onToggleNav: () => void;
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
/** The left navigation column: desktop title, mobile switcher, and the body. */
|
|
318
|
-
export function SettingsRail({
|
|
319
|
-
title,
|
|
320
|
-
activeLabel,
|
|
321
|
-
navOpen,
|
|
322
|
-
onToggleNav,
|
|
323
|
-
testIdPrefix,
|
|
324
|
-
...bodyProps
|
|
325
|
-
}: SettingsRailProps): React.JSX.Element {
|
|
326
|
-
const theme = useTheme();
|
|
327
|
-
return (
|
|
328
|
-
<Box
|
|
329
|
-
component="nav"
|
|
330
|
-
aria-label={typeof title === 'string' ? title : 'Settings'}
|
|
331
|
-
data-testid={`${testIdPrefix}-rail`}
|
|
332
|
-
sx={{
|
|
333
|
-
flex: { xs: '0 0 auto', md: `0 0 ${RAIL_WIDTH}px` },
|
|
334
|
-
width: { xs: '100%', md: RAIL_WIDTH },
|
|
335
|
-
borderRight: { md: `1px solid ${theme.palette.divider}` },
|
|
336
|
-
pr: { md: 2 },
|
|
337
|
-
}}
|
|
338
|
-
>
|
|
339
|
-
{title ? (
|
|
340
|
-
<Typography
|
|
341
|
-
variant="h6"
|
|
342
|
-
data-testid={`${testIdPrefix}-title`}
|
|
343
|
-
sx={{ display: { xs: 'none', md: 'block' }, fontWeight: 700, mb: 1.5 }}
|
|
344
|
-
>
|
|
345
|
-
{title}
|
|
346
|
-
</Typography>
|
|
347
|
-
) : null}
|
|
348
|
-
<SettingsRailToggle
|
|
349
|
-
title={title}
|
|
350
|
-
activeLabel={activeLabel}
|
|
351
|
-
open={navOpen}
|
|
352
|
-
onToggle={onToggleNav}
|
|
353
|
-
testIdPrefix={testIdPrefix}
|
|
354
|
-
/>
|
|
355
|
-
<SettingsRailBody navOpen={navOpen} testIdPrefix={testIdPrefix} {...bodyProps} />
|
|
356
|
-
</Box>
|
|
357
|
-
);
|
|
358
|
-
}
|