@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.
Files changed (37) hide show
  1. package/components-catalog.md +7 -0
  2. package/dist/layout/SettingsLayout.js +563 -109
  3. package/dist/layout/SettingsLayout.js.map +1 -1
  4. package/dist/types/components/layout/SettingsLayout/SettingsLayout.d.ts +19 -2
  5. package/dist/types/components/layout/SettingsLayout/SettingsLayout.d.ts.map +1 -1
  6. package/dist/types/components/layout/SettingsLayout/SettingsLayout.filters.d.ts +6 -0
  7. package/dist/types/components/layout/SettingsLayout/SettingsLayout.filters.d.ts.map +1 -0
  8. package/dist/types/components/layout/SettingsLayout/SettingsLayout.items.d.ts +31 -0
  9. package/dist/types/components/layout/SettingsLayout/SettingsLayout.items.d.ts.map +1 -0
  10. package/dist/types/components/layout/SettingsLayout/{SettingsLayout.parts.d.ts → SettingsLayout.rail.d.ts} +6 -9
  11. package/dist/types/components/layout/SettingsLayout/SettingsLayout.rail.d.ts.map +1 -0
  12. package/dist/types/components/layout/SettingsLayout/SettingsLayout.styles.d.ts +35 -0
  13. package/dist/types/components/layout/SettingsLayout/SettingsLayout.styles.d.ts.map +1 -0
  14. package/dist/types/components/layout/SettingsLayout/SettingsLayout.types.d.ts +109 -0
  15. package/dist/types/components/layout/SettingsLayout/SettingsLayout.types.d.ts.map +1 -1
  16. package/dist/types/components/layout/SettingsLayout/SettingsPanel.d.ts +29 -0
  17. package/dist/types/components/layout/SettingsLayout/SettingsPanel.d.ts.map +1 -0
  18. package/dist/types/components/layout/SettingsLayout/SettingsSectionChips.d.ts +26 -0
  19. package/dist/types/components/layout/SettingsLayout/SettingsSectionChips.d.ts.map +1 -0
  20. package/dist/types/components/layout/SettingsLayout/SettingsStatusMarker.d.ts +28 -0
  21. package/dist/types/components/layout/SettingsLayout/SettingsStatusMarker.d.ts.map +1 -0
  22. package/dist/types/components/layout/SettingsLayout/index.d.ts +5 -1
  23. package/dist/types/components/layout/SettingsLayout/index.d.ts.map +1 -1
  24. package/package.json +2 -2
  25. package/src/components/layout/SettingsLayout/SettingsLayout.filters.ts +21 -0
  26. package/src/components/layout/SettingsLayout/SettingsLayout.items.tsx +228 -0
  27. package/src/components/layout/SettingsLayout/SettingsLayout.md +93 -1
  28. package/src/components/layout/SettingsLayout/SettingsLayout.rail.tsx +279 -0
  29. package/src/components/layout/SettingsLayout/SettingsLayout.styles.ts +64 -0
  30. package/src/components/layout/SettingsLayout/SettingsLayout.tsx +118 -25
  31. package/src/components/layout/SettingsLayout/SettingsLayout.types.ts +113 -0
  32. package/src/components/layout/SettingsLayout/SettingsPanel.tsx +140 -0
  33. package/src/components/layout/SettingsLayout/SettingsSectionChips.tsx +0 -0
  34. package/src/components/layout/SettingsLayout/SettingsStatusMarker.tsx +75 -0
  35. package/src/components/layout/SettingsLayout/index.ts +8 -0
  36. package/dist/types/components/layout/SettingsLayout/SettingsLayout.parts.d.ts.map +0 -1
  37. 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
- }