cynx-lib 1.3.82 → 1.3.84
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 +1 -1
- package/ui/components/Layout.jsx +37 -0
- package/ui/components/LoginPage.jsx +142 -26
- package/ui/components/Sidebar.jsx +300 -134
package/package.json
CHANGED
package/ui/components/Layout.jsx
CHANGED
|
@@ -12,6 +12,24 @@ export default function Layout({
|
|
|
12
12
|
user,
|
|
13
13
|
onLogout,
|
|
14
14
|
onLock,
|
|
15
|
+
// Footer customization & actions
|
|
16
|
+
footerActions,
|
|
17
|
+
extraFooterActions,
|
|
18
|
+
footerSlot,
|
|
19
|
+
lockLabel,
|
|
20
|
+
logoutLabel,
|
|
21
|
+
lockTitle,
|
|
22
|
+
logoutTitle,
|
|
23
|
+
lockIcon,
|
|
24
|
+
logoutIcon,
|
|
25
|
+
showLock,
|
|
26
|
+
showLogout,
|
|
27
|
+
userSlot,
|
|
28
|
+
renderUser,
|
|
29
|
+
footerStyle,
|
|
30
|
+
footerClassName,
|
|
31
|
+
footerActionsLayout,
|
|
32
|
+
footerActionsStyle,
|
|
15
33
|
collapsible = true,
|
|
16
34
|
isCollapsed,
|
|
17
35
|
onToggleCollapse,
|
|
@@ -38,6 +56,7 @@ export default function Layout({
|
|
|
38
56
|
contentStyle = {},
|
|
39
57
|
contentClassName,
|
|
40
58
|
children,
|
|
59
|
+
...restProps
|
|
41
60
|
}) {
|
|
42
61
|
return (
|
|
43
62
|
<div
|
|
@@ -59,6 +78,23 @@ export default function Layout({
|
|
|
59
78
|
user={user}
|
|
60
79
|
onLogout={onLogout}
|
|
61
80
|
onLock={onLock}
|
|
81
|
+
footerActions={footerActions}
|
|
82
|
+
extraFooterActions={extraFooterActions}
|
|
83
|
+
footerSlot={footerSlot}
|
|
84
|
+
lockLabel={lockLabel}
|
|
85
|
+
logoutLabel={logoutLabel}
|
|
86
|
+
lockTitle={lockTitle}
|
|
87
|
+
logoutTitle={logoutTitle}
|
|
88
|
+
lockIcon={lockIcon}
|
|
89
|
+
logoutIcon={logoutIcon}
|
|
90
|
+
showLock={showLock}
|
|
91
|
+
showLogout={showLogout}
|
|
92
|
+
userSlot={userSlot}
|
|
93
|
+
renderUser={renderUser}
|
|
94
|
+
footerStyle={footerStyle}
|
|
95
|
+
footerClassName={footerClassName}
|
|
96
|
+
footerActionsLayout={footerActionsLayout}
|
|
97
|
+
footerActionsStyle={footerActionsStyle}
|
|
62
98
|
collapsible={collapsible}
|
|
63
99
|
isCollapsed={isCollapsed}
|
|
64
100
|
onToggleCollapse={onToggleCollapse}
|
|
@@ -75,6 +111,7 @@ export default function Layout({
|
|
|
75
111
|
activeSubColor={activeSubColor}
|
|
76
112
|
subItemsIndent={subItemsIndent}
|
|
77
113
|
hasPermission={hasPermission}
|
|
114
|
+
{...restProps}
|
|
78
115
|
/>
|
|
79
116
|
|
|
80
117
|
{/* Main Content Area */}
|
|
@@ -35,10 +35,15 @@ export default function LoginPage({
|
|
|
35
35
|
passkeyIcon,
|
|
36
36
|
onPasskeyLogin,
|
|
37
37
|
passkeyLoading = false,
|
|
38
|
+
passkeyInGrid = false,
|
|
39
|
+
passkeyPosition = 'start', // 'start' | 'end'
|
|
38
40
|
|
|
39
|
-
// SSO Providers
|
|
41
|
+
// SSO & Alternate Providers
|
|
40
42
|
ssoProviders = [],
|
|
41
|
-
ssoDividerText,
|
|
43
|
+
ssoDividerText = 'или',
|
|
44
|
+
ssoLayout = 'column', // 'column' | 'grid'
|
|
45
|
+
ssoColumns, // e.g. 2, 3, 4, or undefined (auto)
|
|
46
|
+
ssoButtonFormat, // 'full' | 'short' | 'icon-only'
|
|
42
47
|
|
|
43
48
|
// Captcha Configuration
|
|
44
49
|
captcha,
|
|
@@ -61,11 +66,13 @@ export default function LoginPage({
|
|
|
61
66
|
const [pageReady, setPageReady] = useState(false);
|
|
62
67
|
const [capToken, setCapToken] = useState('');
|
|
63
68
|
const [passkeyInternalLoading, setPasskeyInternalLoading] = useState(false);
|
|
69
|
+
const [activeSsoId, setActiveSsoId] = useState(null);
|
|
64
70
|
const capRef = useRef(null);
|
|
65
71
|
|
|
66
72
|
const error = externalError !== undefined ? externalError : internalError;
|
|
67
|
-
const
|
|
73
|
+
const isFormBusy = externalLoading || internalLoading;
|
|
68
74
|
const isPasskeyBusy = passkeyLoading || passkeyInternalLoading;
|
|
75
|
+
const isAnyBusy = isFormBusy || isPasskeyBusy || !!activeSsoId;
|
|
69
76
|
|
|
70
77
|
const isCaptchaEnabled = !!(captcha && (captcha.enabled !== false && (captcha.provider || captcha.endpoint || captcha.siteKey)));
|
|
71
78
|
const captchaProvider = captcha?.provider || 'cap';
|
|
@@ -134,7 +141,7 @@ export default function LoginPage({
|
|
|
134
141
|
// 3. Form Submission
|
|
135
142
|
const handleSubmit = async (e) => {
|
|
136
143
|
if (e && e.preventDefault) e.preventDefault();
|
|
137
|
-
if (
|
|
144
|
+
if (isAnyBusy) return;
|
|
138
145
|
|
|
139
146
|
if (!username.trim() || !password) {
|
|
140
147
|
setInternalError('Введите логин и пароль');
|
|
@@ -170,7 +177,7 @@ export default function LoginPage({
|
|
|
170
177
|
|
|
171
178
|
// 4. Passkey Action Handler
|
|
172
179
|
const handlePasskeyClick = async () => {
|
|
173
|
-
if (
|
|
180
|
+
if (isAnyBusy) return;
|
|
174
181
|
setInternalError('');
|
|
175
182
|
setPasskeyInternalLoading(true);
|
|
176
183
|
try {
|
|
@@ -184,6 +191,21 @@ export default function LoginPage({
|
|
|
184
191
|
}
|
|
185
192
|
};
|
|
186
193
|
|
|
194
|
+
// 5. SSO Action Handler (with independent button loading)
|
|
195
|
+
const handleSsoClick = async (p, e) => {
|
|
196
|
+
if (isAnyBusy) return;
|
|
197
|
+
if (typeof p.onClick !== 'function') return;
|
|
198
|
+
setInternalError('');
|
|
199
|
+
setActiveSsoId(p.id);
|
|
200
|
+
try {
|
|
201
|
+
await p.onClick(e);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
setInternalError(err.message || `Ошибка входа через ${p.name || 'провайдера'}`);
|
|
204
|
+
} finally {
|
|
205
|
+
setActiveSsoId(null);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
187
209
|
if (!pageReady) {
|
|
188
210
|
return (
|
|
189
211
|
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background }}>
|
|
@@ -196,6 +218,29 @@ export default function LoginPage({
|
|
|
196
218
|
const hasPasskey = showPasskey && typeof onPasskeyLogin === 'function';
|
|
197
219
|
const hasAuthMethods = showPasswordLogin || hasPasskey || enabledSSO.length > 0;
|
|
198
220
|
|
|
221
|
+
const isGrid = ssoLayout === 'grid' || (typeof ssoColumns === 'number' && ssoColumns > 1);
|
|
222
|
+
const shouldPutPasskeyInGrid = hasPasskey && (passkeyInGrid || (isGrid && passkeyInGrid !== false && enabledSSO.length > 0));
|
|
223
|
+
|
|
224
|
+
const passkeyProvider = hasPasskey ? {
|
|
225
|
+
id: '__passkey__',
|
|
226
|
+
name: passkeyButtonText,
|
|
227
|
+
label: passkeyButtonText,
|
|
228
|
+
icon: passkeyIcon || <Fingerprint size={16} />,
|
|
229
|
+
onClick: handlePasskeyClick,
|
|
230
|
+
loading: isPasskeyBusy,
|
|
231
|
+
disabled: isAnyBusy && !isPasskeyBusy,
|
|
232
|
+
isPasskey: true,
|
|
233
|
+
} : null;
|
|
234
|
+
|
|
235
|
+
const allAlternateProviders = shouldPutPasskeyInGrid
|
|
236
|
+
? (passkeyPosition === 'end'
|
|
237
|
+
? [...enabledSSO, passkeyProvider]
|
|
238
|
+
: [passkeyProvider, ...enabledSSO]
|
|
239
|
+
)
|
|
240
|
+
: enabledSSO;
|
|
241
|
+
|
|
242
|
+
const hasAlternateMethods = (hasPasskey && !shouldPutPasskeyInGrid) || allAlternateProviders.length > 0;
|
|
243
|
+
|
|
199
244
|
return (
|
|
200
245
|
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background, padding: 16 }}>
|
|
201
246
|
<div style={{ width: '100%', maxWidth: cardMaxWidth }}>
|
|
@@ -309,7 +354,8 @@ export default function LoginPage({
|
|
|
309
354
|
type="button"
|
|
310
355
|
variant="primary"
|
|
311
356
|
size="lg"
|
|
312
|
-
loading={
|
|
357
|
+
loading={isFormBusy}
|
|
358
|
+
disabled={isAnyBusy && !isFormBusy}
|
|
313
359
|
onClick={handleSubmit}
|
|
314
360
|
style={{
|
|
315
361
|
width: '100%',
|
|
@@ -319,19 +365,27 @@ export default function LoginPage({
|
|
|
319
365
|
fontWeight: 600,
|
|
320
366
|
}}
|
|
321
367
|
>
|
|
322
|
-
{
|
|
368
|
+
{isFormBusy ? submitButtonLoadingText : submitButtonText}
|
|
323
369
|
</Button>
|
|
324
370
|
</div>
|
|
325
371
|
)}
|
|
326
372
|
|
|
327
|
-
{/*
|
|
328
|
-
{
|
|
329
|
-
<
|
|
373
|
+
{/* Divider between password login and alternate auth methods */}
|
|
374
|
+
{showPasswordLogin && hasAlternateMethods && (
|
|
375
|
+
<Divider color="var(--border)" size="md" style={{ margin: '18px 0' }}>
|
|
376
|
+
{ssoDividerText}
|
|
377
|
+
</Divider>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{/* Standalone Passkey Action (when not placed in grid) */}
|
|
381
|
+
{hasPasskey && !shouldPutPasskeyInGrid && (
|
|
382
|
+
<div style={{ marginBottom: allAlternateProviders.length > 0 ? 8 : 0 }}>
|
|
330
383
|
<Button
|
|
331
384
|
type="button"
|
|
332
385
|
variant="default"
|
|
333
386
|
size="lg"
|
|
334
387
|
loading={isPasskeyBusy}
|
|
388
|
+
disabled={isAnyBusy && !isPasskeyBusy}
|
|
335
389
|
onClick={handlePasskeyClick}
|
|
336
390
|
style={{
|
|
337
391
|
width: '100%',
|
|
@@ -348,45 +402,107 @@ export default function LoginPage({
|
|
|
348
402
|
</div>
|
|
349
403
|
)}
|
|
350
404
|
|
|
351
|
-
{/* SSO
|
|
352
|
-
{
|
|
353
|
-
<
|
|
354
|
-
{
|
|
355
|
-
|
|
356
|
-
|
|
405
|
+
{/* Alternate Auth Providers (SSO, Social, Passkey in grid) */}
|
|
406
|
+
{allAlternateProviders.length > 0 && (
|
|
407
|
+
<div
|
|
408
|
+
style={isGrid ? {
|
|
409
|
+
display: 'grid',
|
|
410
|
+
gridTemplateColumns: typeof ssoColumns === 'number'
|
|
411
|
+
? `repeat(${ssoColumns}, minmax(0, 1fr))`
|
|
412
|
+
: `repeat(auto-fit, minmax(${cardMaxWidth > 360 ? '130px' : '90px'}, 1fr))`,
|
|
413
|
+
gap: 8,
|
|
414
|
+
width: '100%',
|
|
415
|
+
} : {
|
|
416
|
+
display: 'flex',
|
|
417
|
+
flexDirection: 'column',
|
|
418
|
+
gap: 8,
|
|
419
|
+
width: '100%',
|
|
420
|
+
}}
|
|
421
|
+
>
|
|
422
|
+
{allAlternateProviders.map((p) => {
|
|
423
|
+
const iconNode = p.logo ? (
|
|
424
|
+
typeof p.logo === 'string' ? (
|
|
425
|
+
<img
|
|
426
|
+
src={p.logo}
|
|
427
|
+
alt={p.name || 'provider'}
|
|
428
|
+
style={{ width: 16, height: 16, objectFit: 'contain', flexShrink: 0, display: 'block' }}
|
|
429
|
+
/>
|
|
430
|
+
) : (
|
|
431
|
+
p.logo
|
|
432
|
+
)
|
|
433
|
+
) : (
|
|
434
|
+
p.icon || (p.isPasskey ? <Fingerprint size={16} /> : <Lock size={15} />)
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
let labelText = '';
|
|
438
|
+
if (ssoButtonFormat === 'icon-only' || p.iconOnly) {
|
|
439
|
+
labelText = '';
|
|
440
|
+
} else if (p.label) {
|
|
441
|
+
labelText = p.label;
|
|
442
|
+
} else if (p.buttonText) {
|
|
443
|
+
labelText = p.buttonText;
|
|
444
|
+
} else if (isGrid || ssoButtonFormat === 'short') {
|
|
445
|
+
labelText = p.name;
|
|
446
|
+
} else {
|
|
447
|
+
labelText = (p.name && (p.name.startsWith('Войти') || p.name.startsWith('Sign in')))
|
|
448
|
+
? p.name
|
|
449
|
+
: `Войти через ${p.name}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const isItemLoading = p.isPasskey
|
|
453
|
+
? isPasskeyBusy
|
|
454
|
+
: (p.loading !== undefined ? p.loading : activeSsoId === p.id);
|
|
455
|
+
|
|
456
|
+
const isItemDisabled = p.isPasskey
|
|
457
|
+
? (isAnyBusy && !isPasskeyBusy)
|
|
458
|
+
: (p.disabled !== undefined ? p.disabled : (isAnyBusy && activeSsoId !== p.id));
|
|
459
|
+
|
|
460
|
+
const handleClick = p.isPasskey ? p.onClick : (e) => handleSsoClick(p, e);
|
|
357
461
|
|
|
358
|
-
{/* SSO Providers */}
|
|
359
|
-
{enabledSSO.length > 0 && (
|
|
360
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
361
|
-
{enabledSSO.map((p) => {
|
|
362
462
|
const buttonNode = (
|
|
363
463
|
<Button
|
|
464
|
+
type="button"
|
|
364
465
|
variant={p.variant || 'default'}
|
|
365
466
|
size="lg"
|
|
366
|
-
|
|
467
|
+
loading={isItemLoading}
|
|
468
|
+
disabled={isItemDisabled}
|
|
469
|
+
onClick={handleClick}
|
|
367
470
|
style={{
|
|
368
471
|
width: '100%',
|
|
472
|
+
display: 'flex',
|
|
473
|
+
alignItems: 'center',
|
|
474
|
+
justifyContent: 'center',
|
|
475
|
+
gap: 8,
|
|
369
476
|
color: 'var(--primary-text)',
|
|
370
477
|
background: 'var(--surface)',
|
|
371
478
|
border: '1px solid var(--border)',
|
|
372
479
|
fontWeight: 600,
|
|
373
|
-
fontSize: '.85rem',
|
|
480
|
+
fontSize: isGrid ? '.82rem' : '.85rem',
|
|
481
|
+
padding: isGrid ? (labelText ? '0 10px' : '0 6px') : '0 16px',
|
|
482
|
+
textOverflow: 'ellipsis',
|
|
483
|
+
whiteSpace: 'nowrap',
|
|
484
|
+
overflow: 'hidden',
|
|
485
|
+
...p.style,
|
|
374
486
|
}}
|
|
375
|
-
icon={
|
|
487
|
+
icon={iconNode}
|
|
376
488
|
>
|
|
377
|
-
|
|
489
|
+
{labelText}
|
|
378
490
|
</Button>
|
|
379
491
|
);
|
|
380
492
|
|
|
381
493
|
if (p.url) {
|
|
382
494
|
return (
|
|
383
|
-
<a key={p.id} href={p.url} style={{ textDecoration: 'none', display: 'block' }}>
|
|
495
|
+
<a key={p.id} href={p.url} style={{ textDecoration: 'none', display: 'block', minWidth: 0 }}>
|
|
384
496
|
{buttonNode}
|
|
385
497
|
</a>
|
|
386
498
|
);
|
|
387
499
|
}
|
|
388
500
|
|
|
389
|
-
return
|
|
501
|
+
return (
|
|
502
|
+
<div key={p.id} style={{ minWidth: 0 }}>
|
|
503
|
+
{buttonNode}
|
|
504
|
+
</div>
|
|
505
|
+
);
|
|
390
506
|
})}
|
|
391
507
|
</div>
|
|
392
508
|
)}
|
|
@@ -102,6 +102,23 @@ export default function Sidebar({
|
|
|
102
102
|
user,
|
|
103
103
|
onLogout,
|
|
104
104
|
onLock,
|
|
105
|
+
// Footer customization & actions
|
|
106
|
+
footerActions,
|
|
107
|
+
extraFooterActions,
|
|
108
|
+
lockLabel = 'Блок',
|
|
109
|
+
logoutLabel = 'Выйти',
|
|
110
|
+
lockTitle,
|
|
111
|
+
logoutTitle,
|
|
112
|
+
lockIcon,
|
|
113
|
+
logoutIcon,
|
|
114
|
+
showLock = true,
|
|
115
|
+
showLogout = true,
|
|
116
|
+
userSlot,
|
|
117
|
+
renderUser,
|
|
118
|
+
footerStyle = {},
|
|
119
|
+
footerClassName = '',
|
|
120
|
+
footerActionsLayout = 'flex',
|
|
121
|
+
footerActionsStyle = {},
|
|
105
122
|
collapsible = true,
|
|
106
123
|
isCollapsed: controlledIsCollapsed,
|
|
107
124
|
defaultCollapsed = false,
|
|
@@ -260,6 +277,70 @@ export default function Sidebar({
|
|
|
260
277
|
? 'var(--sidebar-text-muted, rgba(255, 255, 255, 0.45))'
|
|
261
278
|
: 'var(--sidebar-text-muted, var(--t3, #a1a1aa))';
|
|
262
279
|
|
|
280
|
+
// Default footer actions (Lock / Logout)
|
|
281
|
+
const defaultActions = [];
|
|
282
|
+
|
|
283
|
+
if (onLock && showLock) {
|
|
284
|
+
defaultActions.push({
|
|
285
|
+
id: 'lock',
|
|
286
|
+
label: lockLabel,
|
|
287
|
+
title: lockTitle || (typeof lockLabel === 'string' ? lockLabel : 'Заблокировать'),
|
|
288
|
+
icon: lockIcon || (isCollapsed ? <Lock size={16} /> : <Lock size={13} />),
|
|
289
|
+
onClick: onLock,
|
|
290
|
+
variant: isDarkSidebar ? 'secondary' : 'ghost',
|
|
291
|
+
style: isDarkSidebar
|
|
292
|
+
? {
|
|
293
|
+
background: 'rgba(255,255,255,0.08)',
|
|
294
|
+
color: 'rgba(255,255,255,0.85)',
|
|
295
|
+
borderColor: 'rgba(255,255,255,0.12)',
|
|
296
|
+
}
|
|
297
|
+
: {},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (onLogout && showLogout) {
|
|
302
|
+
defaultActions.push({
|
|
303
|
+
id: 'logout',
|
|
304
|
+
label: logoutLabel,
|
|
305
|
+
title:
|
|
306
|
+
logoutTitle ||
|
|
307
|
+
(user?.username || user?.fullName
|
|
308
|
+
? `${typeof logoutLabel === 'string' ? logoutLabel : 'Выйти'} (${user?.username || user?.fullName})`
|
|
309
|
+
: typeof logoutLabel === 'string'
|
|
310
|
+
? logoutLabel
|
|
311
|
+
: 'Выйти'),
|
|
312
|
+
icon: logoutIcon || (isCollapsed ? <LogOut size={17} /> : <LogOut size={14} />),
|
|
313
|
+
onClick: onLogout,
|
|
314
|
+
variant: 'ghost',
|
|
315
|
+
danger: true,
|
|
316
|
+
style: isDarkSidebar
|
|
317
|
+
? {
|
|
318
|
+
background: 'rgba(239, 68, 68, 0.1)',
|
|
319
|
+
borderColor: 'rgba(239, 68, 68, 0.2)',
|
|
320
|
+
}
|
|
321
|
+
: {},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Resolve final actions list
|
|
326
|
+
let resolvedActions;
|
|
327
|
+
if (typeof footerActions === 'function') {
|
|
328
|
+
resolvedActions = footerActions(defaultActions, { isCollapsed, user, isDarkSidebar });
|
|
329
|
+
} else if (Array.isArray(footerActions)) {
|
|
330
|
+
resolvedActions = footerActions;
|
|
331
|
+
} else {
|
|
332
|
+
resolvedActions = [...defaultActions];
|
|
333
|
+
if (Array.isArray(extraFooterActions) && extraFooterActions.length > 0) {
|
|
334
|
+
resolvedActions.push(...extraFooterActions);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const finalActions = (resolvedActions || []).filter(
|
|
339
|
+
(act) => act && !act.hidden && act.visible !== false
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const hasFooterContent = Boolean(user || finalActions.length > 0 || userSlot || renderUser);
|
|
343
|
+
|
|
263
344
|
return (
|
|
264
345
|
<aside
|
|
265
346
|
className={sidebarClassName}
|
|
@@ -659,158 +740,243 @@ export default function Sidebar({
|
|
|
659
740
|
|
|
660
741
|
{/* Footer Slot or Default User Profile / Actions */}
|
|
661
742
|
{footerSlot ? (
|
|
662
|
-
footerSlot
|
|
663
|
-
|
|
743
|
+
typeof footerSlot === 'function'
|
|
744
|
+
? footerSlot({ isCollapsed, user, actions: finalActions, isDarkSidebar })
|
|
745
|
+
: footerSlot
|
|
746
|
+
) : hasFooterContent ? (
|
|
664
747
|
<div
|
|
748
|
+
className={footerClassName}
|
|
665
749
|
style={{
|
|
666
750
|
padding: isCollapsed ? '12px 8px' : 12,
|
|
667
751
|
borderTop: effectiveSidebarBorder,
|
|
668
752
|
flexShrink: 0,
|
|
753
|
+
...footerStyle,
|
|
669
754
|
}}
|
|
670
755
|
>
|
|
671
756
|
{!isCollapsed ? (
|
|
672
757
|
<>
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
color: userFullNameColor,
|
|
677
|
-
marginBottom: 8,
|
|
678
|
-
padding: '0 8px',
|
|
679
|
-
whiteSpace: 'nowrap',
|
|
680
|
-
overflow: 'hidden',
|
|
681
|
-
textOverflow: 'ellipsis',
|
|
682
|
-
}}
|
|
683
|
-
>
|
|
684
|
-
{user.fullName ? (
|
|
685
|
-
<>
|
|
686
|
-
{user.fullName}{' '}
|
|
687
|
-
{user.username && (
|
|
688
|
-
<span style={{ color: userUsernameColor }}>({user.username})</span>
|
|
689
|
-
)}
|
|
690
|
-
</>
|
|
758
|
+
{userSlot ? (
|
|
759
|
+
typeof userSlot === 'function' ? (
|
|
760
|
+
userSlot({ user, isCollapsed: false, isDarkSidebar })
|
|
691
761
|
) : (
|
|
692
|
-
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
<Button
|
|
699
|
-
variant={isDarkSidebar ? 'secondary' : 'ghost'}
|
|
700
|
-
size="md"
|
|
701
|
-
onClick={onLock}
|
|
702
|
-
icon={<Lock size={13} />}
|
|
703
|
-
title="Заблокировать"
|
|
704
|
-
style={{
|
|
705
|
-
flex: 1,
|
|
706
|
-
justifyContent: 'center',
|
|
707
|
-
...(isDarkSidebar
|
|
708
|
-
? {
|
|
709
|
-
background: 'rgba(255,255,255,0.08)',
|
|
710
|
-
color: 'rgba(255,255,255,0.85)',
|
|
711
|
-
borderColor: 'rgba(255,255,255,0.12)',
|
|
712
|
-
}
|
|
713
|
-
: {}),
|
|
714
|
-
}}
|
|
715
|
-
>
|
|
716
|
-
Блок
|
|
717
|
-
</Button>
|
|
718
|
-
)}
|
|
719
|
-
|
|
720
|
-
{onLogout && (
|
|
721
|
-
<Button
|
|
722
|
-
variant="ghost"
|
|
723
|
-
size="md"
|
|
724
|
-
onClick={onLogout}
|
|
725
|
-
icon={<LogOut size={14} />}
|
|
726
|
-
title="Выйти"
|
|
727
|
-
style={{
|
|
728
|
-
width: onLock ? 'auto' : '100%',
|
|
729
|
-
flex: onLock ? 1 : undefined,
|
|
730
|
-
justifyContent: 'center',
|
|
731
|
-
color: isDarkSidebar ? '#f87171' : 'var(--danger, #ef4444)',
|
|
732
|
-
...(isDarkSidebar
|
|
733
|
-
? {
|
|
734
|
-
background: 'rgba(239, 68, 68, 0.1)',
|
|
735
|
-
borderColor: 'rgba(239, 68, 68, 0.2)',
|
|
736
|
-
}
|
|
737
|
-
: {}),
|
|
738
|
-
}}
|
|
739
|
-
>
|
|
740
|
-
Выйти
|
|
741
|
-
</Button>
|
|
742
|
-
)}
|
|
743
|
-
</div>
|
|
744
|
-
</>
|
|
745
|
-
) : (
|
|
746
|
-
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
|
|
747
|
-
{onLock && (
|
|
748
|
-
<button
|
|
749
|
-
type="button"
|
|
750
|
-
onClick={onLock}
|
|
751
|
-
title="Заблокировать экран"
|
|
762
|
+
userSlot
|
|
763
|
+
)
|
|
764
|
+
) : renderUser ? (
|
|
765
|
+
renderUser({ user, isCollapsed: false, isDarkSidebar })
|
|
766
|
+
) : user ? (
|
|
767
|
+
<div
|
|
752
768
|
style={{
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
borderRadius: 'var(--rads, 8px)',
|
|
761
|
-
color: isDarkSidebar ? 'rgba(255,255,255,0.7)' : 'var(--t2, #71717a)',
|
|
762
|
-
cursor: 'pointer',
|
|
763
|
-
transition: 'all .14s',
|
|
764
|
-
}}
|
|
765
|
-
onMouseEnter={(e) => {
|
|
766
|
-
e.currentTarget.style.background = isDarkSidebar
|
|
767
|
-
? 'rgba(255,255,255,0.08)'
|
|
768
|
-
: 'var(--grey-1, #fafafa)';
|
|
769
|
-
e.currentTarget.style.color = isDarkSidebar
|
|
770
|
-
? '#ffffff'
|
|
771
|
-
: 'var(--primary-text, #09090b)';
|
|
772
|
-
}}
|
|
773
|
-
onMouseLeave={(e) => {
|
|
774
|
-
e.currentTarget.style.background = 'transparent';
|
|
775
|
-
e.currentTarget.style.color = isDarkSidebar
|
|
776
|
-
? 'rgba(255,255,255,0.7)'
|
|
777
|
-
: 'var(--t2, #71717a)';
|
|
769
|
+
fontSize: '.72rem',
|
|
770
|
+
color: userFullNameColor,
|
|
771
|
+
marginBottom: 8,
|
|
772
|
+
padding: '0 8px',
|
|
773
|
+
whiteSpace: 'nowrap',
|
|
774
|
+
overflow: 'hidden',
|
|
775
|
+
textOverflow: 'ellipsis',
|
|
778
776
|
}}
|
|
779
777
|
>
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
778
|
+
{user.fullName ? (
|
|
779
|
+
<>
|
|
780
|
+
{user.fullName}{' '}
|
|
781
|
+
{user.username && (
|
|
782
|
+
<span style={{ color: userUsernameColor }}>({user.username})</span>
|
|
783
|
+
)}
|
|
784
|
+
</>
|
|
785
|
+
) : (
|
|
786
|
+
user.username
|
|
787
|
+
)}
|
|
788
|
+
</div>
|
|
789
|
+
) : null}
|
|
783
790
|
|
|
784
|
-
{
|
|
785
|
-
<
|
|
786
|
-
type="button"
|
|
787
|
-
onClick={onLogout}
|
|
788
|
-
title={`Выйти (${user?.username || user?.fullName || ''})`}
|
|
791
|
+
{finalActions.length > 0 && (
|
|
792
|
+
<div
|
|
789
793
|
style={{
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}}
|
|
807
|
-
onMouseLeave={(e) => {
|
|
808
|
-
e.currentTarget.style.background = 'transparent';
|
|
794
|
+
...(footerActionsLayout === 'column'
|
|
795
|
+
? { display: 'flex', flexDirection: 'column', gap: 6, width: '100%' }
|
|
796
|
+
: footerActionsLayout === 'grid'
|
|
797
|
+
? {
|
|
798
|
+
display: 'grid',
|
|
799
|
+
gridTemplateColumns: `repeat(${finalActions.length > 2 ? 2 : finalActions.length}, minmax(0, 1fr))`,
|
|
800
|
+
gap: 6,
|
|
801
|
+
width: '100%',
|
|
802
|
+
}
|
|
803
|
+
: {
|
|
804
|
+
display: 'flex',
|
|
805
|
+
gap: 6,
|
|
806
|
+
flexWrap: 'wrap',
|
|
807
|
+
width: '100%',
|
|
808
|
+
}),
|
|
809
|
+
...footerActionsStyle,
|
|
809
810
|
}}
|
|
810
811
|
>
|
|
811
|
-
|
|
812
|
-
|
|
812
|
+
{finalActions.map((act, index) => {
|
|
813
|
+
const key = act.id || act.key || index;
|
|
814
|
+
if (act.render) {
|
|
815
|
+
return (
|
|
816
|
+
<React.Fragment key={key}>
|
|
817
|
+
{act.render({ action: act, isCollapsed: false, isDarkSidebar, user })}
|
|
818
|
+
</React.Fragment>
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const isDanger = Boolean(act.danger || act.variant === 'danger');
|
|
823
|
+
const actionLabel =
|
|
824
|
+
act.label !== undefined
|
|
825
|
+
? act.label
|
|
826
|
+
: act.text !== undefined
|
|
827
|
+
? act.text
|
|
828
|
+
: act.title;
|
|
829
|
+
const actionTitle =
|
|
830
|
+
act.title || (typeof actionLabel === 'string' ? actionLabel : undefined);
|
|
831
|
+
const isSingle = finalActions.length === 1;
|
|
832
|
+
|
|
833
|
+
return (
|
|
834
|
+
<Button
|
|
835
|
+
key={key}
|
|
836
|
+
variant={
|
|
837
|
+
act.variant || (isDanger ? 'ghost' : isDarkSidebar ? 'secondary' : 'ghost')
|
|
838
|
+
}
|
|
839
|
+
size={act.size || 'md'}
|
|
840
|
+
onClick={act.onClick}
|
|
841
|
+
disabled={act.disabled}
|
|
842
|
+
icon={act.icon}
|
|
843
|
+
title={actionTitle}
|
|
844
|
+
className={act.className || ''}
|
|
845
|
+
style={{
|
|
846
|
+
flex: act.flex !== undefined ? act.flex : 1,
|
|
847
|
+
width: isSingle ? '100%' : (act.width || 'auto'),
|
|
848
|
+
justifyContent: act.justifyContent || 'center',
|
|
849
|
+
minWidth: 0,
|
|
850
|
+
whiteSpace: 'nowrap',
|
|
851
|
+
...(isDanger
|
|
852
|
+
? {
|
|
853
|
+
color: isDarkSidebar ? '#f87171' : 'var(--danger, #ef4444)',
|
|
854
|
+
...(isDarkSidebar
|
|
855
|
+
? {
|
|
856
|
+
background: 'rgba(239, 68, 68, 0.1)',
|
|
857
|
+
borderColor: 'rgba(239, 68, 68, 0.2)',
|
|
858
|
+
}
|
|
859
|
+
: {}),
|
|
860
|
+
}
|
|
861
|
+
: isDarkSidebar &&
|
|
862
|
+
(!act.variant || act.variant === 'secondary' || act.variant === 'ghost')
|
|
863
|
+
? {
|
|
864
|
+
background: 'rgba(255,255,255,0.08)',
|
|
865
|
+
color: 'rgba(255,255,255,0.85)',
|
|
866
|
+
borderColor: 'rgba(255,255,255,0.12)',
|
|
867
|
+
}
|
|
868
|
+
: {}),
|
|
869
|
+
...act.style,
|
|
870
|
+
}}
|
|
871
|
+
>
|
|
872
|
+
{actionLabel}
|
|
873
|
+
</Button>
|
|
874
|
+
);
|
|
875
|
+
})}
|
|
876
|
+
</div>
|
|
813
877
|
)}
|
|
878
|
+
</>
|
|
879
|
+
) : (
|
|
880
|
+
<div
|
|
881
|
+
style={{
|
|
882
|
+
display: 'flex',
|
|
883
|
+
flexDirection: 'column',
|
|
884
|
+
alignItems: 'center',
|
|
885
|
+
gap: 6,
|
|
886
|
+
width: '100%',
|
|
887
|
+
}}
|
|
888
|
+
>
|
|
889
|
+
{userSlot &&
|
|
890
|
+
typeof userSlot === 'function' &&
|
|
891
|
+
userSlot({ user, isCollapsed: true, isDarkSidebar })}
|
|
892
|
+
{renderUser && renderUser({ user, isCollapsed: true, isDarkSidebar })}
|
|
893
|
+
{finalActions.map((act, index) => {
|
|
894
|
+
const key = act.id || act.key || index;
|
|
895
|
+
if (act.render) {
|
|
896
|
+
return (
|
|
897
|
+
<React.Fragment key={key}>
|
|
898
|
+
{act.render({ action: act, isCollapsed: true, isDarkSidebar, user })}
|
|
899
|
+
</React.Fragment>
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const isDanger = Boolean(act.danger || act.variant === 'danger');
|
|
904
|
+
const actionLabel =
|
|
905
|
+
act.label !== undefined
|
|
906
|
+
? act.label
|
|
907
|
+
: act.text !== undefined
|
|
908
|
+
? act.text
|
|
909
|
+
: act.title;
|
|
910
|
+
const actionTitle =
|
|
911
|
+
act.title || (typeof actionLabel === 'string' ? actionLabel : undefined);
|
|
912
|
+
const actionIcon =
|
|
913
|
+
act.collapsedIcon ||
|
|
914
|
+
act.icon ||
|
|
915
|
+
(typeof actionLabel === 'string' ? actionLabel.slice(0, 2) : null);
|
|
916
|
+
|
|
917
|
+
const normalColor = isDanger
|
|
918
|
+
? isDarkSidebar
|
|
919
|
+
? '#f87171'
|
|
920
|
+
: 'var(--danger, #ef4444)'
|
|
921
|
+
: isDarkSidebar
|
|
922
|
+
? 'rgba(255,255,255,0.7)'
|
|
923
|
+
: 'var(--t2, #71717a)';
|
|
924
|
+
|
|
925
|
+
const hoverBg = isDanger
|
|
926
|
+
? isDarkSidebar
|
|
927
|
+
? 'rgba(239,68,68,0.2)'
|
|
928
|
+
: 'var(--danger-bg, rgba(239,68,68,.10))'
|
|
929
|
+
: isDarkSidebar
|
|
930
|
+
? 'rgba(255,255,255,0.08)'
|
|
931
|
+
: 'var(--grey-1, #fafafa)';
|
|
932
|
+
|
|
933
|
+
const hoverColor = isDanger
|
|
934
|
+
? isDarkSidebar
|
|
935
|
+
? '#fca5a5'
|
|
936
|
+
: 'var(--danger, #ef4444)'
|
|
937
|
+
: isDarkSidebar
|
|
938
|
+
? '#ffffff'
|
|
939
|
+
: 'var(--primary-text, #09090b)';
|
|
940
|
+
|
|
941
|
+
return (
|
|
942
|
+
<button
|
|
943
|
+
key={key}
|
|
944
|
+
type="button"
|
|
945
|
+
onClick={act.onClick}
|
|
946
|
+
disabled={act.disabled}
|
|
947
|
+
title={actionTitle}
|
|
948
|
+
aria-label={actionTitle}
|
|
949
|
+
className={act.className || ''}
|
|
950
|
+
style={{
|
|
951
|
+
width: 36,
|
|
952
|
+
height: 36,
|
|
953
|
+
display: 'flex',
|
|
954
|
+
alignItems: 'center',
|
|
955
|
+
justifyContent: 'center',
|
|
956
|
+
background: 'transparent',
|
|
957
|
+
border: 'none',
|
|
958
|
+
borderRadius: 'var(--rads, 8px)',
|
|
959
|
+
color: normalColor,
|
|
960
|
+
cursor: act.disabled ? 'not-allowed' : 'pointer',
|
|
961
|
+
opacity: act.disabled ? 0.5 : 1,
|
|
962
|
+
transition: 'all .14s',
|
|
963
|
+
...act.collapsedStyle,
|
|
964
|
+
...act.style,
|
|
965
|
+
}}
|
|
966
|
+
onMouseEnter={(e) => {
|
|
967
|
+
if (act.disabled) return;
|
|
968
|
+
e.currentTarget.style.background = hoverBg;
|
|
969
|
+
e.currentTarget.style.color = hoverColor;
|
|
970
|
+
}}
|
|
971
|
+
onMouseLeave={(e) => {
|
|
972
|
+
e.currentTarget.style.background = 'transparent';
|
|
973
|
+
e.currentTarget.style.color = normalColor;
|
|
974
|
+
}}
|
|
975
|
+
>
|
|
976
|
+
{actionIcon}
|
|
977
|
+
</button>
|
|
978
|
+
);
|
|
979
|
+
})}
|
|
814
980
|
</div>
|
|
815
981
|
)}
|
|
816
982
|
</div>
|