cynx-lib 1.3.50 → 1.3.52

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.
@@ -0,0 +1,820 @@
1
+ import React, { useState } from 'react';
2
+ import { NavLink } from 'react-router-dom';
3
+ import {
4
+ PanelLeftClose,
5
+ PanelLeftOpen,
6
+ LogOut,
7
+ ChevronDown,
8
+ Lock,
9
+ } from 'lucide-react';
10
+ import Button from './Button.jsx';
11
+
12
+ export function isDarkColor(color) {
13
+ if (!color || typeof color !== 'string') return false;
14
+ let c = color.trim().toLowerCase();
15
+ if (c === 'transparent' || c === 'inherit') return false;
16
+
17
+ // Resolve CSS variable if running in browser
18
+ if (c.startsWith('var(') && typeof window !== 'undefined') {
19
+ let depth = 0;
20
+ while (c.startsWith('var(') && depth < 5) {
21
+ const varNameMatch = c.match(/var\(\s*(--[a-zA-Z0-9_-]+)(?:,\s*([^)]+))?\)/);
22
+ if (varNameMatch) {
23
+ const varName = varNameMatch[1];
24
+ const fallback = varNameMatch[2] ? varNameMatch[2].trim() : '';
25
+ const resolved = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
26
+ if (resolved) {
27
+ c = resolved.toLowerCase();
28
+ } else if (fallback) {
29
+ c = fallback.toLowerCase();
30
+ } else {
31
+ break;
32
+ }
33
+ } else {
34
+ break;
35
+ }
36
+ depth++;
37
+ }
38
+ }
39
+
40
+ // Hex color (#rgb, #rgba, #rrggbb, #rrggbbaa)
41
+ if (c.startsWith('#')) {
42
+ let hex = c.slice(1);
43
+ if (hex.length === 3 || hex.length === 4) {
44
+ hex = hex.split('').slice(0, 3).map((x) => x + x).join('');
45
+ }
46
+ if (hex.length >= 6) {
47
+ const r = parseInt(hex.slice(0, 2), 16);
48
+ const g = parseInt(hex.slice(2, 4), 16);
49
+ const b = parseInt(hex.slice(4, 6), 16);
50
+ const luma = 0.299 * r + 0.587 * g + 0.114 * b;
51
+ return luma < 145;
52
+ }
53
+ }
54
+
55
+ // RGB/RGBA color
56
+ if (c.startsWith('rgb')) {
57
+ const match = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
58
+ if (match) {
59
+ const r = parseInt(match[1], 10);
60
+ const g = parseInt(match[2], 10);
61
+ const b = parseInt(match[3], 10);
62
+ const luma = 0.299 * r + 0.587 * g + 0.114 * b;
63
+ return luma < 145;
64
+ }
65
+ }
66
+
67
+ // HSL color
68
+ if (c.startsWith('hsl')) {
69
+ const match = c.match(/hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*(\d+)%/);
70
+ if (match) {
71
+ const lightness = parseInt(match[1], 10);
72
+ return lightness < 50;
73
+ }
74
+ }
75
+
76
+ // Named dark colors or dark CSS hints
77
+ if (
78
+ c === '#18181b' ||
79
+ c === '#141629' ||
80
+ c === '#09090b' ||
81
+ c === '#0f172a' ||
82
+ c === '#1e293b' ||
83
+ c === '#111827' ||
84
+ c === '#1f2937' ||
85
+ c === 'black' ||
86
+ c === '#000000' ||
87
+ c === '#000' ||
88
+ c.includes('dark') ||
89
+ c.includes('black') ||
90
+ c.includes('navy')
91
+ ) {
92
+ return true;
93
+ }
94
+
95
+ return false;
96
+ }
97
+
98
+ export default function Sidebar({
99
+ brand,
100
+ navSections = [],
101
+ navItems = [],
102
+ user,
103
+ onLogout,
104
+ onLock,
105
+ collapsible = true,
106
+ isCollapsed: controlledIsCollapsed,
107
+ defaultCollapsed = false,
108
+ defaultSectionsExpanded = false,
109
+ sectionsCollapsible = true,
110
+ autoExpandActive = true,
111
+ onToggleCollapse,
112
+ onToggleSection,
113
+ storageKey = 'app_sidebar_collapsed',
114
+ // Sidebar styling props
115
+ sidebarBg,
116
+ sidebarColor,
117
+ sidebarMutedColor,
118
+ sidebarBorder,
119
+ sidebarStyle = {},
120
+ sidebarClassName = '',
121
+ // Active states
122
+ activeBg,
123
+ activeColor,
124
+ activeSubBg,
125
+ activeSubColor,
126
+ subItemsIndent = 12,
127
+ hasPermission = () => true,
128
+ headerSlot,
129
+ footerSlot,
130
+ children,
131
+ }) {
132
+ const [internalCollapsed, setInternalCollapsed] = useState(() => {
133
+ if (typeof window !== 'undefined' && storageKey) {
134
+ const saved = localStorage.getItem(storageKey);
135
+ if (saved !== null) return saved === 'true';
136
+ }
137
+ return defaultCollapsed;
138
+ });
139
+
140
+ const isCollapsed =
141
+ typeof controlledIsCollapsed === 'boolean' ? controlledIsCollapsed : internalCollapsed;
142
+
143
+ const [expandedSections, setExpandedSections] = useState({});
144
+
145
+ const toggleSidebarCollapse = (e) => {
146
+ if (e) {
147
+ e.preventDefault();
148
+ e.stopPropagation();
149
+ }
150
+ const next = !isCollapsed;
151
+ if (onToggleCollapse) {
152
+ onToggleCollapse(next);
153
+ } else {
154
+ setInternalCollapsed(next);
155
+ }
156
+ if (typeof window !== 'undefined' && storageKey) {
157
+ localStorage.setItem(storageKey, String(next));
158
+ }
159
+ };
160
+
161
+ const isSectionCollapsible = (sec) => {
162
+ if (typeof sec.isCollapsible === 'boolean') return sec.isCollapsible;
163
+ if (typeof sec.collapsible === 'boolean') return sec.collapsible;
164
+ return sectionsCollapsible;
165
+ };
166
+
167
+ const isSectionExpanded = (sec, secIdx) => {
168
+ if (!isSectionCollapsible(sec)) {
169
+ return true;
170
+ }
171
+ if (typeof expandedSections[secIdx] === 'boolean') {
172
+ return expandedSections[secIdx];
173
+ }
174
+ if (typeof sec.defaultExpanded === 'boolean') return sec.defaultExpanded;
175
+ if (typeof sec.defaultCollapsed === 'boolean') return !sec.defaultCollapsed;
176
+ if (typeof sec.expanded === 'boolean') return sec.expanded;
177
+ if (typeof sec.collapsed === 'boolean') return !sec.collapsed;
178
+ return defaultSectionsExpanded;
179
+ };
180
+
181
+ const toggleSection = (sec, secIdx) => {
182
+ if (!isSectionCollapsible(sec)) return;
183
+ const current = isSectionExpanded(sec, secIdx);
184
+ const next = !current;
185
+ setExpandedSections((prev) => ({
186
+ ...prev,
187
+ [secIdx]: next,
188
+ }));
189
+ if (onToggleSection) {
190
+ onToggleSection({ section: sec, index: secIdx, expanded: next });
191
+ }
192
+ };
193
+
194
+ const sections = React.useMemo(() => {
195
+ if (Array.isArray(navSections) && navSections.length > 0) return navSections;
196
+ if (Array.isArray(navItems) && navItems.length > 0) return [{ items: navItems }];
197
+ return [];
198
+ }, [navSections, navItems]);
199
+
200
+ // Contrast & palette calculations
201
+ const effectiveSidebarBg = sidebarBg || 'var(--sidebar-bg, var(--surface, #ffffff))';
202
+ const isDarkSidebar = isDarkColor(effectiveSidebarBg);
203
+
204
+ const effectiveSidebarBorder =
205
+ sidebarBorder ||
206
+ (isDarkSidebar
207
+ ? '1px solid var(--sidebar-border, rgba(255, 255, 255, 0.08))'
208
+ : '1px solid var(--sidebar-border, var(--border-light, #f4f4f5))');
209
+
210
+ const brandTitleColor =
211
+ sidebarColor ||
212
+ (isDarkSidebar
213
+ ? 'var(--sidebar-text, #ffffff)'
214
+ : 'var(--sidebar-text, var(--primary-text, #09090b))');
215
+
216
+ const brandSubtitleColor =
217
+ sidebarMutedColor ||
218
+ (isDarkSidebar
219
+ ? 'var(--sidebar-text-muted, rgba(255, 255, 255, 0.45))'
220
+ : 'var(--sidebar-text-muted, var(--muted, #71717a))');
221
+
222
+ const toggleBtnColor = isDarkSidebar
223
+ ? 'var(--sidebar-text-muted, rgba(255, 255, 255, 0.65))'
224
+ : 'var(--sidebar-text-muted, var(--t2, #71717a))';
225
+ const toggleBtnHoverBg = isDarkSidebar
226
+ ? 'var(--sidebar-item-hover, rgba(255, 255, 255, 0.08))'
227
+ : 'var(--sidebar-item-hover, var(--grey-1, #fafafa))';
228
+ const toggleBtnHoverColor = isDarkSidebar
229
+ ? 'var(--sidebar-text, #ffffff)'
230
+ : 'var(--sidebar-text, var(--primary-text, #09090b))';
231
+
232
+ const sectionHeaderColor =
233
+ sidebarMutedColor ||
234
+ (isDarkSidebar
235
+ ? 'var(--sidebar-text-muted, rgba(255, 255, 255, 0.45))'
236
+ : 'var(--sidebar-text-muted, var(--muted, #71717a))');
237
+
238
+ const inactiveItemColor = isDarkSidebar
239
+ ? 'var(--sidebar-text-muted, rgba(255, 255, 255, 0.65))'
240
+ : 'var(--sidebar-text-muted, var(--t2, #71717a))';
241
+ const inactiveItemHoverBg = isDarkSidebar
242
+ ? 'var(--sidebar-item-hover, rgba(255, 255, 255, 0.06))'
243
+ : 'var(--sidebar-item-hover, var(--grey-1, #fafafa))';
244
+ const inactiveItemHoverColor = isDarkSidebar
245
+ ? 'var(--sidebar-text, #ffffff)'
246
+ : 'var(--sidebar-text, var(--primary-text, #09090b))';
247
+
248
+ const defaultActiveBg = isDarkSidebar
249
+ ? 'var(--sidebar-item-active-bg, rgba(255, 255, 255, 0.12))'
250
+ : 'var(--sidebar-item-active-bg, var(--grey-1, #fafafa))';
251
+
252
+ const defaultActiveColor = isDarkSidebar
253
+ ? 'var(--sidebar-item-active-text, #ffffff)'
254
+ : 'var(--sidebar-item-active-text, var(--primary-text, #09090b))';
255
+
256
+ const userFullNameColor = isDarkSidebar
257
+ ? 'var(--sidebar-text, rgba(255, 255, 255, 0.85))'
258
+ : 'var(--sidebar-text-muted, var(--muted, #71717a))';
259
+ const userUsernameColor = isDarkSidebar
260
+ ? 'var(--sidebar-text-muted, rgba(255, 255, 255, 0.45))'
261
+ : 'var(--sidebar-text-muted, var(--t3, #a1a1aa))';
262
+
263
+ return (
264
+ <aside
265
+ className={sidebarClassName}
266
+ style={{
267
+ width: isCollapsed ? 64 : 240,
268
+ minWidth: isCollapsed ? 64 : 240,
269
+ height: '100vh',
270
+ background: effectiveSidebarBg,
271
+ borderRight: effectiveSidebarBorder,
272
+ display: 'flex',
273
+ flexDirection: 'column',
274
+ boxSizing: 'border-box',
275
+ userSelect: 'none',
276
+ transition:
277
+ 'width 0.2s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.2s cubic-bezier(0.4, 0, 0.2, 1), background 0.2s ease',
278
+ overflow: 'hidden',
279
+ flexShrink: 0,
280
+ ...sidebarStyle,
281
+ }}
282
+ >
283
+ {/* Header Slot or Default Brand Header */}
284
+ {headerSlot ? (
285
+ headerSlot
286
+ ) : (
287
+ <div
288
+ style={{
289
+ height: 56,
290
+ minHeight: 56,
291
+ display: 'flex',
292
+ alignItems: 'center',
293
+ justifyContent: isCollapsed ? 'center' : 'space-between',
294
+ padding: isCollapsed ? '0 8px' : '0 12px 0 16px',
295
+ borderBottom: effectiveSidebarBorder,
296
+ boxSizing: 'border-box',
297
+ flexShrink: 0,
298
+ }}
299
+ >
300
+ {!isCollapsed ? (
301
+ <>
302
+ <div
303
+ style={{
304
+ flex: 1,
305
+ minWidth: 0,
306
+ display: 'flex',
307
+ alignItems: 'center',
308
+ gap: 10,
309
+ overflow: 'hidden',
310
+ }}
311
+ >
312
+ {brand?.logoUrl ? (
313
+ <img
314
+ src={brand.logoUrl}
315
+ alt=""
316
+ style={{ maxHeight: 32, maxWidth: 145, objectFit: 'contain' }}
317
+ />
318
+ ) : (
319
+ <>
320
+ {brand?.icon && <div style={{ flexShrink: 0 }}>{brand.icon}</div>}
321
+ <div style={{ display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
322
+ <span
323
+ style={{
324
+ fontWeight: 700,
325
+ fontSize: '.88rem',
326
+ color: brandTitleColor,
327
+ overflow: 'hidden',
328
+ textOverflow: 'ellipsis',
329
+ whiteSpace: 'nowrap',
330
+ }}
331
+ >
332
+ {brand?.title || 'Приложение'}
333
+ </span>
334
+ {brand?.subtitle && (
335
+ <span
336
+ style={{
337
+ fontSize: '.64rem',
338
+ color: brandSubtitleColor,
339
+ fontWeight: 500,
340
+ }}
341
+ >
342
+ {brand.subtitle}
343
+ </span>
344
+ )}
345
+ </div>
346
+ </>
347
+ )}
348
+ </div>
349
+
350
+ {collapsible && (
351
+ <button
352
+ type="button"
353
+ onClick={toggleSidebarCollapse}
354
+ title="Свернуть меню"
355
+ style={{
356
+ background: 'transparent',
357
+ border: 'none',
358
+ cursor: 'pointer',
359
+ color: toggleBtnColor,
360
+ display: 'flex',
361
+ alignItems: 'center',
362
+ justifyContent: 'center',
363
+ width: 30,
364
+ height: 30,
365
+ borderRadius: 'var(--radxs, 6px)',
366
+ padding: 0,
367
+ transition: 'background .14s, color .14s',
368
+ flexShrink: 0,
369
+ marginLeft: 8,
370
+ }}
371
+ onMouseEnter={(e) => {
372
+ e.currentTarget.style.background = toggleBtnHoverBg;
373
+ e.currentTarget.style.color = toggleBtnHoverColor;
374
+ }}
375
+ onMouseLeave={(e) => {
376
+ e.currentTarget.style.background = 'transparent';
377
+ e.currentTarget.style.color = toggleBtnColor;
378
+ }}
379
+ >
380
+ <PanelLeftClose size={18} />
381
+ </button>
382
+ )}
383
+ </>
384
+ ) : (
385
+ collapsible && (
386
+ <button
387
+ type="button"
388
+ onClick={toggleSidebarCollapse}
389
+ title="Развернуть меню"
390
+ style={{
391
+ background: 'transparent',
392
+ border: 'none',
393
+ cursor: 'pointer',
394
+ color: toggleBtnColor,
395
+ display: 'flex',
396
+ alignItems: 'center',
397
+ justifyContent: 'center',
398
+ width: 36,
399
+ height: 36,
400
+ borderRadius: 'var(--radxs, 6px)',
401
+ padding: 0,
402
+ transition: 'background .14s, color .14s',
403
+ }}
404
+ onMouseEnter={(e) => {
405
+ e.currentTarget.style.background = toggleBtnHoverBg;
406
+ e.currentTarget.style.color = toggleBtnHoverColor;
407
+ }}
408
+ onMouseLeave={(e) => {
409
+ e.currentTarget.style.background = 'transparent';
410
+ e.currentTarget.style.color = toggleBtnColor;
411
+ }}
412
+ >
413
+ <PanelLeftOpen size={18} />
414
+ </button>
415
+ )
416
+ )}
417
+ </div>
418
+ )}
419
+
420
+ {/* Sidebar Navigation */}
421
+ <nav
422
+ style={{
423
+ padding: isCollapsed ? '12px 8px' : 12,
424
+ flex: 1,
425
+ display: 'flex',
426
+ flexDirection: 'column',
427
+ gap: 4,
428
+ overflowY: 'auto',
429
+ overflowX: 'hidden',
430
+ }}
431
+ >
432
+ {sections.map((sec, secIdx) => {
433
+ const visibleItems = (sec.items || []).filter(
434
+ (it) => !it.permission || hasPermission(it.permission)
435
+ );
436
+ if (visibleItems.length === 0) return null;
437
+
438
+ const isCollapsibleSec = isSectionCollapsible(sec);
439
+ const isExpanded = isSectionExpanded(sec, secIdx);
440
+
441
+ return (
442
+ <div key={secIdx} style={{ marginTop: secIdx > 0 && !isCollapsed ? 6 : 0 }}>
443
+ {/* Section Header: Accordion Button (if collapsible) or Static Title (if not collapsible) */}
444
+ {sec.title && !isCollapsed && (
445
+ isCollapsibleSec ? (
446
+ <button
447
+ type="button"
448
+ onClick={() => toggleSection(sec, secIdx)}
449
+ style={{
450
+ width: '100%',
451
+ display: 'flex',
452
+ alignItems: 'center',
453
+ justifyContent: 'space-between',
454
+ padding: '6px 10px',
455
+ borderRadius: 'var(--rads, 8px)',
456
+ fontSize: '.7rem',
457
+ fontWeight: 600,
458
+ textTransform: 'uppercase',
459
+ letterSpacing: '.06em',
460
+ color: sectionHeaderColor,
461
+ background: 'transparent',
462
+ border: 'none',
463
+ cursor: 'pointer',
464
+ boxSizing: 'border-box',
465
+ }}
466
+ onMouseEnter={(e) => {
467
+ if (isDarkSidebar) {
468
+ e.currentTarget.style.background = 'rgba(255, 255, 255, 0.04)';
469
+ }
470
+ }}
471
+ onMouseLeave={(e) => {
472
+ e.currentTarget.style.background = 'transparent';
473
+ }}
474
+ >
475
+ <span>{sec.title}</span>
476
+ <ChevronDown
477
+ size={13}
478
+ style={{
479
+ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
480
+ transition: 'transform 0.22s cubic-bezier(0.4, 0, 0.2, 1)',
481
+ opacity: isDarkSidebar ? 0.75 : 0.6,
482
+ }}
483
+ />
484
+ </button>
485
+ ) : (
486
+ <div
487
+ style={{
488
+ width: '100%',
489
+ display: 'flex',
490
+ alignItems: 'center',
491
+ padding: '6px 10px',
492
+ fontSize: '.7rem',
493
+ fontWeight: 600,
494
+ textTransform: 'uppercase',
495
+ letterSpacing: '.06em',
496
+ color: sectionHeaderColor,
497
+ boxSizing: 'border-box',
498
+ cursor: 'default',
499
+ }}
500
+ >
501
+ <span>{sec.title}</span>
502
+ </div>
503
+ )
504
+ )}
505
+
506
+ {/* Separator when collapsed */}
507
+ {sec.title && isCollapsed && secIdx > 0 && (
508
+ <div
509
+ style={{
510
+ height: 1,
511
+ background: isDarkSidebar
512
+ ? 'var(--sidebar-border, rgba(255, 255, 255, 0.08))'
513
+ : 'var(--sidebar-border, var(--border-light, #f4f4f5))',
514
+ margin: '4px 0',
515
+ }}
516
+ />
517
+ )}
518
+
519
+ {/* Smooth 60fps Grid Container for Accordion Items */}
520
+ <div
521
+ style={{
522
+ display: 'grid',
523
+ gridTemplateRows: isCollapsed || isExpanded ? '1fr' : '0fr',
524
+ transition: 'grid-template-rows 0.22s cubic-bezier(0.4, 0, 0.2, 1)',
525
+ }}
526
+ >
527
+ <div
528
+ style={{
529
+ overflow: 'hidden',
530
+ display: 'flex',
531
+ flexDirection: 'column',
532
+ gap: 2,
533
+ paddingTop: sec.title && !isCollapsed ? 2 : 0,
534
+ paddingLeft: sec.title && !isCollapsed && subItemsIndent ? subItemsIndent : 0,
535
+ }}
536
+ >
537
+ {visibleItems.map((item) => {
538
+ const Icon = item.icon;
539
+
540
+ // Active styles computation
541
+ const effectiveActiveBg =
542
+ item.activeBg ||
543
+ (sec.title ? activeSubBg : null) ||
544
+ activeBg ||
545
+ defaultActiveBg;
546
+
547
+ const isDarkActiveBg = isDarkColor(effectiveActiveBg);
548
+
549
+ const effectiveActiveColor =
550
+ item.activeColor ||
551
+ (sec.title ? activeSubColor : null) ||
552
+ activeColor ||
553
+ (isDarkActiveBg ? '#ffffff' : defaultActiveColor);
554
+
555
+ return (
556
+ <NavLink
557
+ key={item.to}
558
+ to={item.to}
559
+ end={item.end}
560
+ title={isCollapsed ? item.label : undefined}
561
+ style={({ isActive }) => {
562
+ const currentBg = isActive ? effectiveActiveBg : 'transparent';
563
+ const currentColor = isActive ? effectiveActiveColor : inactiveItemColor;
564
+
565
+ return {
566
+ display: 'flex',
567
+ alignItems: 'center',
568
+ justifyContent: isCollapsed ? 'center' : 'space-between',
569
+ padding: isCollapsed
570
+ ? '9px 0'
571
+ : sec.title
572
+ ? '6px 8px'
573
+ : '8px 10px',
574
+ borderRadius: 'var(--rads, 8px)',
575
+ fontSize: sec.title && !isCollapsed ? '.79rem' : '.82rem',
576
+ fontWeight: isActive ? 600 : sec.title ? 400 : 500,
577
+ color: currentColor,
578
+ background: currentBg,
579
+ border: 'none',
580
+ textDecoration: 'none',
581
+ cursor: 'pointer',
582
+ textAlign: 'left',
583
+ fontFamily: 'inherit',
584
+ transition: 'all .14s',
585
+ boxSizing: 'border-box',
586
+ outline: 'none',
587
+ };
588
+ }}
589
+ onMouseEnter={(e) => {
590
+ if (!e.currentTarget.classList.contains('active')) {
591
+ e.currentTarget.style.background = inactiveItemHoverBg;
592
+ e.currentTarget.style.color = inactiveItemHoverColor;
593
+ }
594
+ }}
595
+ onMouseLeave={(e) => {
596
+ if (!e.currentTarget.classList.contains('active')) {
597
+ e.currentTarget.style.background = 'transparent';
598
+ e.currentTarget.style.color = inactiveItemColor;
599
+ }
600
+ }}
601
+ >
602
+ {({ isActive }) => {
603
+ const iconSize = isCollapsed ? 18 : sec.title ? 15 : 16;
604
+
605
+ return (
606
+ <>
607
+ <div style={{ display: 'flex', alignItems: 'center', gap: sec.title ? 8 : 10 }}>
608
+ {Icon && <Icon size={iconSize} style={{ flexShrink: 0 }} />}
609
+ {!isCollapsed && <span>{item.label}</span>}
610
+ </div>
611
+
612
+ {!isCollapsed && typeof item.count === 'number' && (
613
+ <span
614
+ style={{
615
+ fontSize: sec.title ? '.64rem' : '.68rem',
616
+ padding: '1px 6px',
617
+ borderRadius: 'var(--radxs, 4px)',
618
+ background: isActive
619
+ ? isDarkActiveBg
620
+ ? 'rgba(255,255,255,0.2)'
621
+ : isDarkSidebar
622
+ ? 'rgba(255,255,255,0.15)'
623
+ : 'var(--border, #e4e4e7)'
624
+ : isDarkSidebar
625
+ ? 'rgba(255,255,255,0.08)'
626
+ : 'var(--grey-1, #fafafa)',
627
+ color: isActive
628
+ ? isDarkActiveBg || isDarkSidebar
629
+ ? '#ffffff'
630
+ : 'var(--primary-text, #09090b)'
631
+ : isDarkSidebar
632
+ ? 'rgba(255,255,255,0.6)'
633
+ : 'var(--muted, #71717a)',
634
+ fontFamily: 'var(--mono, monospace)',
635
+ border:
636
+ isActive && (isDarkActiveBg || isDarkSidebar)
637
+ ? 'none'
638
+ : isDarkSidebar
639
+ ? '1px solid var(--sidebar-border, rgba(255,255,255,0.08))'
640
+ : '1px solid var(--sidebar-border, var(--border-light, #f4f4f5))',
641
+ }}
642
+ >
643
+ {item.count}
644
+ </span>
645
+ )}
646
+ </>
647
+ );
648
+ }}
649
+ </NavLink>
650
+ );
651
+ })}
652
+ </div>
653
+ </div>
654
+ </div>
655
+ );
656
+ })}
657
+ {children}
658
+ </nav>
659
+
660
+ {/* Footer Slot or Default User Profile / Actions */}
661
+ {footerSlot ? (
662
+ footerSlot
663
+ ) : user ? (
664
+ <div
665
+ style={{
666
+ padding: isCollapsed ? '12px 8px' : 12,
667
+ borderTop: effectiveSidebarBorder,
668
+ flexShrink: 0,
669
+ }}
670
+ >
671
+ {!isCollapsed ? (
672
+ <>
673
+ <div
674
+ style={{
675
+ fontSize: '.72rem',
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
+ </>
691
+ ) : (
692
+ user.username
693
+ )}
694
+ </div>
695
+
696
+ <div style={{ display: 'flex', gap: 6 }}>
697
+ {onLock && (
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="Заблокировать экран"
752
+ style={{
753
+ width: 36,
754
+ height: 36,
755
+ display: 'flex',
756
+ alignItems: 'center',
757
+ justifyContent: 'center',
758
+ background: 'transparent',
759
+ border: 'none',
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)';
778
+ }}
779
+ >
780
+ <Lock size={16} />
781
+ </button>
782
+ )}
783
+
784
+ {onLogout && (
785
+ <button
786
+ type="button"
787
+ onClick={onLogout}
788
+ title={`Выйти (${user?.username || user?.fullName || ''})`}
789
+ style={{
790
+ width: 36,
791
+ height: 36,
792
+ display: 'flex',
793
+ alignItems: 'center',
794
+ justifyContent: 'center',
795
+ background: 'transparent',
796
+ border: 'none',
797
+ borderRadius: 'var(--rads, 8px)',
798
+ color: isDarkSidebar ? '#f87171' : 'var(--danger, #ef4444)',
799
+ cursor: 'pointer',
800
+ transition: 'all .14s',
801
+ }}
802
+ onMouseEnter={(e) => {
803
+ e.currentTarget.style.background = isDarkSidebar
804
+ ? 'rgba(239,68,68,0.2)'
805
+ : 'var(--danger-bg, rgba(239,68,68,.10))';
806
+ }}
807
+ onMouseLeave={(e) => {
808
+ e.currentTarget.style.background = 'transparent';
809
+ }}
810
+ >
811
+ <LogOut size={17} />
812
+ </button>
813
+ )}
814
+ </div>
815
+ )}
816
+ </div>
817
+ ) : null}
818
+ </aside>
819
+ );
820
+ }