cynx-lib 1.3.44 → 1.3.45

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