@wealthx/shadcn 1.5.33 → 1.5.34

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": "@wealthx/shadcn",
3
- "version": "1.5.33",
3
+ "version": "1.5.34",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -5,11 +5,12 @@
5
5
  * Used by both Backoffice (Wealth Pro) and Frontend (WealthX app).
6
6
  *
7
7
  * Background: bg-brand-secondary (tenant dark navy — set via ThemeProvider).
8
- * Text: text-brand-brand-secondary-foreground (white on dark navy).
8
+ * Text: text-brand-secondary-foreground (white on dark navy).
9
9
  *
10
10
  * - All icons must be Lucide icons (LucideIcon type).
11
11
  * - Supports collapsible sub-items (accordion).
12
- * - Collapsed state: icon-only, metrics animated out.
12
+ * - Hover mode (default): sidebar is icon-only; hovering expands it temporarily.
13
+ * - Lock mode: clicking the Pin button keeps the sidebar expanded after mouse leaves.
13
14
  * - metricsGroups: optional financial summary rows (Frontend sidebar only).
14
15
  * - No internal navigation — consumers wire onNavigate / onLogout.
15
16
  */
@@ -19,8 +20,8 @@ import {
19
20
  ChevronRight,
20
21
  Info,
21
22
  LogOut,
22
- PanelLeftClose,
23
- PanelLeftOpen,
23
+ Pin,
24
+ PinOff,
24
25
  } from "lucide-react";
25
26
  import type { LucideIcon } from "lucide-react";
26
27
  import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
@@ -72,8 +73,6 @@ export interface SidebarNavProps {
72
73
  items: SidebarNavItem[];
73
74
  /** Display name for the current user */
74
75
  userName?: string;
75
- /** Whether the sidebar is in narrow (icon-only) mode */
76
- collapsed?: boolean;
77
76
  /**
78
77
  * Optional logo URL rendered at the top of the sidebar.
79
78
  * Hidden when collapsed (unless logoCollapsed is provided).
@@ -88,13 +87,21 @@ export interface SidebarNavProps {
88
87
  logoCollapsed?: string;
89
88
  /**
90
89
  * Optional financial metric groups rendered between the user section and
91
- * nav items. Hidden when collapsed. Used by the Frontend (WealthX app) sidebar.
90
+ * nav items. Hidden when sidebar is collapsed. Used by the Frontend (WealthX app) sidebar.
92
91
  */
93
92
  metricsGroups?: SidebarNavMetricsGroup[];
94
93
  onNavigate?: (href: string) => void;
95
94
  onLogout?: () => void;
96
- /** Called when the user clicks the expand/collapse toggle button at the bottom */
97
- onCollapsedChange?: (collapsed: boolean) => void;
95
+ /**
96
+ * Initial locked (pinned) state. When true, sidebar starts expanded and stays
97
+ * open even without hover. Defaults to false (hover-to-expand mode).
98
+ */
99
+ defaultLocked?: boolean;
100
+ /**
101
+ * Called when the user toggles the Pin/Unpin button.
102
+ * Use to persist the preference (e.g. localStorage).
103
+ */
104
+ onLockedChange?: (locked: boolean) => void;
98
105
  className?: string;
99
106
  }
100
107
 
@@ -105,7 +112,7 @@ function navIconCn(isActive: boolean): string {
105
112
  "shrink-0 transition-colors",
106
113
  isActive
107
114
  ? "text-primary"
108
- : "text-brand-secondary-foreground/50 group-hover:text-brand-secondary-foreground",
115
+ : "text-brand-secondary-foreground/50 group-hover:text-brand-secondary-foreground"
109
116
  );
110
117
  }
111
118
 
@@ -147,7 +154,7 @@ function MetricsGroup({ group }: MetricsGroupProps) {
147
154
  className={cn(
148
155
  "text-sm truncate text-brand-secondary-foreground/80",
149
156
  item.isNetItem &&
150
- "font-semibold text-brand-secondary-foreground border-b-2 border-primary pb-px",
157
+ "font-semibold text-brand-secondary-foreground border-b-2 border-primary pb-px"
151
158
  )}
152
159
  >
153
160
  {item.name}
@@ -163,7 +170,7 @@ function MetricsGroup({ group }: MetricsGroupProps) {
163
170
  <span
164
171
  className={cn(
165
172
  "text-sm font-semibold tabular-nums shrink-0 text-brand-secondary-foreground",
166
- item.isNetItem && item.value < 0 && "text-destructive",
173
+ item.isNetItem && item.value < 0 && "text-destructive"
167
174
  )}
168
175
  >
169
176
  {formatCurrency(item.value, { showSign: item.isNetItem })}
@@ -204,8 +211,8 @@ function SidebarNavItemView({
204
211
  "justify-start px-3 border-l-4",
205
212
  item.isActive
206
213
  ? "bg-white/15 text-brand-secondary-foreground border-primary"
207
- : "border-transparent",
208
- ),
214
+ : "border-transparent"
215
+ )
209
216
  )}
210
217
  >
211
218
  <Icon
@@ -250,7 +257,7 @@ function CollapsibleNavItem({
250
257
  className={cn(
251
258
  "group h-auto w-full justify-center px-2 py-2.5 transition-colors",
252
259
  "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground",
253
- hasActiveChild && "bg-white/15 text-brand-secondary-foreground",
260
+ hasActiveChild && "bg-white/15 text-brand-secondary-foreground"
254
261
  )}
255
262
  >
256
263
  <Icon
@@ -276,7 +283,7 @@ function CollapsibleNavItem({
276
283
  "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground",
277
284
  "border-l-4 border-transparent",
278
285
  hasActiveChild &&
279
- "bg-white/15 text-brand-secondary-foreground border-primary",
286
+ "bg-white/15 text-brand-secondary-foreground border-primary"
280
287
  )}
281
288
  >
282
289
  <Icon
@@ -288,7 +295,7 @@ function CollapsibleNavItem({
288
295
  <ChevronDown
289
296
  className={cn(
290
297
  "ml-auto shrink-0 text-brand-secondary-foreground/40 transition-transform duration-200",
291
- "group-data-[panel-open]:rotate-180",
298
+ "group-data-[panel-open]:rotate-180"
292
299
  )}
293
300
  size={14}
294
301
  strokeWidth={2}
@@ -308,7 +315,7 @@ function CollapsibleNavItem({
308
315
  className={cn(
309
316
  "h-auto w-full justify-start gap-2 py-1.5 pl-1 text-sm transition-colors",
310
317
  "text-brand-secondary-foreground/50 hover:text-brand-secondary-foreground",
311
- sub.isActive && "text-primary font-medium",
318
+ sub.isActive && "text-primary font-medium"
312
319
  )}
313
320
  >
314
321
  <ChevronRight
@@ -318,7 +325,7 @@ function CollapsibleNavItem({
318
325
  "shrink-0",
319
326
  sub.isActive
320
327
  ? "text-primary"
321
- : "text-brand-secondary-foreground/30",
328
+ : "text-brand-secondary-foreground/30"
322
329
  )}
323
330
  />
324
331
  <span className="truncate">{sub.title}</span>
@@ -337,48 +344,63 @@ function CollapsibleNavItem({
337
344
  export function SidebarNav({
338
345
  items,
339
346
  userName = "Anonymous User",
340
- collapsed = false,
341
347
  logo,
342
348
  logoCollapsed,
343
349
  metricsGroups,
344
350
  onNavigate,
345
351
  onLogout,
346
- onCollapsedChange,
352
+ defaultLocked = false,
353
+ onLockedChange,
347
354
  className,
348
355
  }: SidebarNavProps) {
356
+ const [isLocked, setIsLocked] = React.useState(defaultLocked);
357
+ const [isHovered, setIsHovered] = React.useState(false);
358
+
359
+ // Sidebar is expanded when pinned OR when the user is hovering over it.
360
+ const isExpanded = isLocked || isHovered;
361
+
349
362
  const [userMenuOpen, setUserMenuOpen] = React.useState(false);
350
363
  const navScrollRef = React.useRef<HTMLDivElement>(null);
351
364
  const expandedScrollRef = React.useRef(0);
352
365
 
366
+ const handleLockToggle = () => {
367
+ const next = !isLocked;
368
+ setIsLocked(next);
369
+ onLockedChange?.(next);
370
+ };
371
+
353
372
  React.useEffect(() => {
354
- if (collapsed) setUserMenuOpen(false);
355
- }, [collapsed]);
373
+ if (!isExpanded) setUserMenuOpen(false);
374
+ }, [isExpanded]);
356
375
 
357
376
  // Preserve nav items scroll position across collapse/expand transitions.
358
377
  // Cleanup saves scrollTop before the DOM changes; setup restores it when expanding.
359
378
  React.useLayoutEffect(() => {
360
379
  const nav = navScrollRef.current;
361
380
  if (!nav) return;
362
- if (!collapsed) {
381
+ if (isExpanded) {
363
382
  nav.scrollTop = expandedScrollRef.current;
364
383
  }
365
384
  return () => {
366
- if (!collapsed && nav) {
385
+ if (isExpanded && nav) {
367
386
  expandedScrollRef.current = nav.scrollTop;
368
387
  }
369
388
  };
370
- }, [collapsed]);
389
+ }, [isExpanded]);
371
390
 
372
391
  return (
373
392
  <TooltipProvider>
374
393
  <nav
375
394
  data-slot="sidebar-nav"
376
- data-collapsed={collapsed}
395
+ data-expanded={isExpanded}
396
+ data-locked={isLocked}
397
+ onMouseEnter={() => setIsHovered(true)}
398
+ onMouseLeave={() => setIsHovered(false)}
377
399
  className={cn(
378
400
  "flex h-full flex-col bg-brand-secondary text-brand-secondary-foreground",
379
401
  "transition-all duration-200 ease-in-out",
380
- collapsed ? "w-14" : "w-[279px]",
381
- className,
402
+ isExpanded ? "w-[279px]" : "w-14",
403
+ className
382
404
  )}
383
405
  >
384
406
  {/* Logo — crossfade between full and icon variant */}
@@ -390,7 +412,7 @@ export function SidebarNav({
390
412
  alt="Logo"
391
413
  className={cn(
392
414
  "h-8 w-auto object-contain object-left px-5 transition-opacity duration-200",
393
- collapsed ? "opacity-0" : "opacity-100",
415
+ !isExpanded ? "opacity-0" : "opacity-100"
394
416
  )}
395
417
  />
396
418
  )}
@@ -400,7 +422,7 @@ export function SidebarNav({
400
422
  alt="Logo"
401
423
  className={cn(
402
424
  "absolute inset-y-0 left-0 right-0 m-auto h-8 w-8 object-contain transition-opacity duration-200",
403
- collapsed ? "opacity-100" : "opacity-0",
425
+ !isExpanded ? "opacity-100" : "opacity-0"
404
426
  )}
405
427
  />
406
428
  )}
@@ -412,7 +434,7 @@ export function SidebarNav({
412
434
  {/* Expanded — in flow (defines height), no transition (overlay handles it) */}
413
435
  <div
414
436
  className={cn(
415
- collapsed ? "opacity-0 pointer-events-none" : "opacity-100",
437
+ !isExpanded ? "opacity-0 pointer-events-none" : "opacity-100"
416
438
  )}
417
439
  >
418
440
  <Accordion
@@ -424,7 +446,7 @@ export function SidebarNav({
424
446
  <AccordionPrimitive.Trigger
425
447
  className={cn(
426
448
  "group flex h-auto w-full items-center justify-start gap-3 px-5 py-5 text-base transition-colors",
427
- "text-brand-secondary-foreground hover:bg-white/10",
449
+ "text-brand-secondary-foreground hover:bg-white/10"
428
450
  )}
429
451
  >
430
452
  <div className="flex h-8 w-8 shrink-0 items-center justify-center font-semibold text-xs bg-primary text-primary-foreground">
@@ -449,7 +471,7 @@ export function SidebarNav({
449
471
  onClick={onLogout}
450
472
  className={cn(
451
473
  "h-auto w-full justify-start gap-3 px-5 py-3 text-base",
452
- "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground transition-colors",
474
+ "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground transition-colors"
453
475
  )}
454
476
  >
455
477
  <LogOut
@@ -466,11 +488,11 @@ export function SidebarNav({
466
488
  </div>
467
489
 
468
490
  {/* Collapsed — absolute overlay, centered avatar */}
469
- <NavTooltip label={userName} collapsed={collapsed}>
491
+ <NavTooltip label={userName} collapsed={!isExpanded}>
470
492
  <div
471
493
  className={cn(
472
494
  "absolute inset-0 flex items-center justify-center transition-opacity duration-200",
473
- collapsed ? "opacity-100" : "opacity-0 pointer-events-none",
495
+ !isExpanded ? "opacity-100" : "opacity-0 pointer-events-none"
474
496
  )}
475
497
  >
476
498
  <div className="flex h-8 w-8 shrink-0 items-center justify-center font-semibold text-xs bg-primary text-primary-foreground">
@@ -480,10 +502,10 @@ export function SidebarNav({
480
502
  </NavTooltip>
481
503
  </div>
482
504
 
483
- {/* Financial metrics — animated in/out with sidebar collapse to prevent nav items jumping */}
505
+ {/* Financial metrics — animated in/out with sidebar expand to prevent nav items jumping */}
484
506
  {!!metricsGroups?.length && (
485
507
  <Accordion
486
- value={!collapsed ? ["metrics"] : []}
508
+ value={isExpanded ? ["metrics"] : []}
487
509
  onValueChange={() => {}}
488
510
  >
489
511
  <AccordionItem className="border-none" value="metrics">
@@ -503,55 +525,54 @@ export function SidebarNav({
503
525
  <CollapsibleNavItem
504
526
  key={item.href}
505
527
  item={item}
506
- collapsed={collapsed}
528
+ collapsed={!isExpanded}
507
529
  onNavigate={onNavigate}
508
530
  />
509
531
  ) : (
510
532
  <SidebarNavItemView
511
533
  key={item.href}
512
534
  item={item}
513
- collapsed={collapsed}
535
+ collapsed={!isExpanded}
514
536
  onNavigate={onNavigate}
515
537
  />
516
- ),
538
+ )
517
539
  )}
518
540
  </div>
519
541
 
520
- {/* Expand/collapse toggle */}
521
- {onCollapsedChange && (
522
- <div className="mt-auto border-t border-white/15 bg-white/8">
523
- <NavTooltip
524
- label={collapsed ? "Expand" : "Collapse"}
525
- collapsed={collapsed}
542
+ {/* Pin / Unpin toggle — always visible */}
543
+ <div className="mt-auto border-t border-white/15 bg-white/10">
544
+ <NavTooltip
545
+ label={isLocked ? "Unpin sidebar" : "Pin sidebar open"}
546
+ collapsed={!isExpanded}
547
+ >
548
+ <Button
549
+ type="button"
550
+ variant="ghost"
551
+ onClick={handleLockToggle}
552
+ className={cn(
553
+ "h-12 w-full items-center gap-3 py-3 transition-colors",
554
+ "text-brand-secondary-foreground/80 hover:bg-white/10 hover:text-brand-secondary-foreground",
555
+ isExpanded ? "justify-start px-3" : "justify-center px-2",
556
+ isLocked && "text-primary"
557
+ )}
526
558
  >
527
- <Button
528
- type="button"
529
- variant="ghost"
530
- onClick={() => onCollapsedChange(!collapsed)}
531
- className={cn(
532
- "h-12 w-full justify-start gap-3 px-3 py-3 transition-colors",
533
- "text-brand-secondary-foreground/80 hover:bg-white/10 hover:text-brand-secondary-foreground",
534
- collapsed && "justify-center px-2",
535
- )}
536
- >
537
- {collapsed ? (
538
- <PanelLeftOpen
539
- size={24}
540
- strokeWidth={1.75}
541
- className="shrink-0"
542
- />
543
- ) : (
544
- <PanelLeftClose
545
- size={24}
546
- strokeWidth={1.75}
547
- className="shrink-0"
548
- />
549
- )}
550
- {!collapsed && <span className="text-sm">Collapse</span>}
551
- </Button>
552
- </NavTooltip>
553
- </div>
554
- )}
559
+ {isLocked ? (
560
+ <Pin size={24} strokeWidth={1.75} className="shrink-0" />
561
+ ) : (
562
+ <PinOff
563
+ size={24}
564
+ strokeWidth={1.75}
565
+ className="shrink-0 opacity-60"
566
+ />
567
+ )}
568
+ {isExpanded && (
569
+ <span className="text-sm">
570
+ {isLocked ? "Pinned" : "Pin sidebar"}
571
+ </span>
572
+ )}
573
+ </Button>
574
+ </NavTooltip>
575
+ </div>
555
576
  </nav>
556
577
  </TooltipProvider>
557
578
  );