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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-lib",
3
- "version": "1.3.82",
3
+ "version": "1.3.84",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -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 loading = externalLoading || internalLoading;
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 (loading) return;
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 (isPasskeyBusy) return;
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={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
- {loading ? submitButtonLoadingText : submitButtonText}
368
+ {isFormBusy ? submitButtonLoadingText : submitButtonText}
323
369
  </Button>
324
370
  </div>
325
371
  )}
326
372
 
327
- {/* Passkey Biometric Action */}
328
- {hasPasskey && (
329
- <div style={{ marginTop: showPasswordLogin ? 10 : 0 }}>
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 Divider */}
352
- {(showPasswordLogin || hasPasskey) && enabledSSO.length > 0 && (
353
- <Divider color="var(--border)" size="md" style={{ margin: '18px 0' }}>
354
- {ssoDividerText}
355
- </Divider>
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
- onClick={p.onClick}
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={p.icon || <Lock size={15} />}
487
+ icon={iconNode}
376
488
  >
377
- Войти через {p.name}
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 <div key={p.id}>{buttonNode}</div>;
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
- ) : user ? (
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
- <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
- </>
758
+ {userSlot ? (
759
+ typeof userSlot === 'function' ? (
760
+ userSlot({ user, isCollapsed: false, isDarkSidebar })
691
761
  ) : (
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="Заблокировать экран"
762
+ userSlot
763
+ )
764
+ ) : renderUser ? (
765
+ renderUser({ user, isCollapsed: false, isDarkSidebar })
766
+ ) : user ? (
767
+ <div
752
768
  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)';
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
- <Lock size={16} />
781
- </button>
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
- {onLogout && (
785
- <button
786
- type="button"
787
- onClick={onLogout}
788
- title={`Выйти (${user?.username || user?.fullName || ''})`}
791
+ {finalActions.length > 0 && (
792
+ <div
789
793
  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';
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
- <LogOut size={17} />
812
- </button>
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>