@syscore/ui-library 1.3.5 → 1.3.7

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.
@@ -75,15 +75,17 @@ interface CardWithIconProps {
75
75
  icon?: React.ComponentType<React.SVGProps<SVGSVGElement> | { className?: string }>;
76
76
  title: string;
77
77
  description: string;
78
+ onClick?: React.MouseEventHandler<HTMLDivElement>;
78
79
  }
79
80
 
80
81
  const CardWithIcon = React.forwardRef<
81
82
  HTMLDivElement,
82
83
  CardWithIconProps
83
- >(({ icon: Icon, title, description }, ref) => (
84
+ >(({ icon: Icon, title, description, onClick }, ref) => (
84
85
  <Card
85
86
  ref={ref}
86
87
  className="card-with-icon"
88
+ onClick={onClick}
87
89
  >
88
90
  <div className="card-with-icon__header">
89
91
  {Icon && <Icon className="card-with-icon__icon" />}
@@ -0,0 +1,578 @@
1
+ import * as React from "react";
2
+ import { AnimatePresence, motion } from "motion/react";
3
+ import { cn } from "@/lib/utils";
4
+ import { UtilityClose } from "../icons/UtilityClose";
5
+ import { NavBullet } from "../icons/NavBullet";
6
+ import { Button } from "@/components/ui/button";
7
+ import { StandardLogo } from "../icons/StandardLogo";
8
+ import { NavLogo } from "../icons/NavLogo";
9
+
10
+ const ALPHA_PATH =
11
+ "M5.3387 0.0229882C5.37971 0.0229882 5.45295 0.0199228 5.5584 0.0137925C5.66386 0.00459714 5.74442 0 5.80007 0C6.72867 0 7.49908 0.269732 8.11131 0.809196C8.72354 1.34559 9.14097 2.09195 9.3636 3.04828C9.44855 3.47433 9.49835 3.96628 9.513 4.52414V5.16782C10.2014 4.07663 10.7287 2.85517 11.0948 1.50345C11.1505 1.29808 11.1959 1.17701 11.2311 1.14023C11.2662 1.10345 11.3761 1.08506 11.5606 1.08506C11.8535 1.08506 12 1.13563 12 1.23678C12 1.25211 11.9722 1.37778 11.9165 1.61379C11.5093 3.24751 10.7931 4.77701 9.76785 6.2023L9.53497 6.51034L9.55694 7.04368C9.59795 8.23295 9.72391 8.9318 9.93482 9.14023C9.97583 9.16782 10.0388 9.18161 10.1238 9.18161C10.32 9.15402 10.5031 9.06973 10.673 8.92874C10.8429 8.78774 10.963 8.62222 11.0333 8.43218C11.0597 8.32797 11.0948 8.26513 11.1388 8.24368C11.1798 8.22222 11.2852 8.21149 11.4551 8.21149C11.7364 8.21149 11.877 8.27739 11.877 8.40919C11.877 8.49808 11.8345 8.63142 11.7495 8.8092C11.5943 9.13103 11.3687 9.4069 11.0729 9.63678C10.777 9.8636 10.4475 9.97701 10.0842 9.97701H9.93482C8.97986 9.97701 8.3398 9.44674 8.01465 8.38621L7.95313 8.23448C7.20615 8.74942 6.79165 9.02835 6.70963 9.07126C5.66679 9.69042 4.61809 10 3.56353 10C2.56756 10 1.76346 9.70575 1.15123 9.11724C0.538997 8.52874 0.162578 7.75632 0.02197 6.8C0.0073233 6.71111 0 6.54866 0 6.31264C0 5.9295 0.02197 5.62146 0.0659099 5.38851C0.276822 4.06437 0.887587 2.8751 1.89821 1.82069C2.91175 0.769348 4.05859 0.170115 5.3387 0.0229882ZM1.83669 7.10805C1.83669 7.73946 1.9978 8.24368 2.32003 8.62069C2.64518 8.99464 3.09484 9.18161 3.66899 9.18161C4.03515 9.18161 4.44379 9.12337 4.89491 9.0069C5.6829 8.80153 6.44892 8.4046 7.19297 7.81609C7.29257 7.74253 7.38777 7.66437 7.47858 7.58161C7.56939 7.50192 7.64262 7.43295 7.69828 7.37471L7.78176 7.28276L7.76419 7.08506C7.76419 6.95326 7.75979 6.75862 7.75101 6.50115C7.74515 6.24368 7.74222 5.98927 7.74222 5.73793C7.72757 5.18008 7.71732 4.74636 7.71146 4.43678C7.70267 4.1272 7.67338 3.74866 7.62358 3.30115C7.57671 2.85364 7.5108 2.50728 7.42585 2.26207C7.3409 2.01992 7.22519 1.77471 7.07873 1.52644C6.92933 1.2751 6.73892 1.09425 6.50751 0.983908C6.27609 0.873563 6.00513 0.81839 5.69462 0.81839C4.72501 0.81839 3.87404 1.35479 3.14171 2.42759C2.79019 2.97318 2.49579 3.71647 2.25851 4.65747C1.9773 5.73333 1.83669 6.55019 1.83669 7.10805Z";
12
+
13
+ const PATH_LENGTH = 60;
14
+
15
+ const AlphaIcon = ({ dark }: { dark?: boolean }) => {
16
+ if (dark) {
17
+ return (
18
+ <svg
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ width="12"
21
+ height="10"
22
+ viewBox="0 0 12 10"
23
+ fill="none"
24
+ className="mt-1.5"
25
+ style={{ overflow: "visible" }}
26
+ >
27
+ <path d={ALPHA_PATH} fill="currentColor" />
28
+ {/* Animated stroke trace */}
29
+ {/* <motion.path
30
+ d={ALPHA_PATH}
31
+ fill="none"
32
+ stroke="#282A31"
33
+ strokeWidth="0.4"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ initial={{ strokeDashoffset: PATH_LENGTH }}
37
+ animate={{ strokeDashoffset: -PATH_LENGTH }}
38
+ style={{ strokeDasharray: `8 ${PATH_LENGTH - 8}` }}
39
+ transition={{
40
+ duration: 4,
41
+ repeat: Infinity,
42
+ ease: "linear",
43
+ }}
44
+ /> */}
45
+ </svg>
46
+ );
47
+ }
48
+
49
+ return (
50
+ <svg
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ width="12"
53
+ height="10"
54
+ viewBox="0 0 12 10"
55
+ fill="none"
56
+ className="mt-1.5"
57
+ style={{ overflow: "visible" }}
58
+ >
59
+ {/* Base fill with gradient */}
60
+ <path
61
+ d={ALPHA_PATH}
62
+ fill="url(#paint0_linear_10081_30848)"
63
+ opacity="0.4"
64
+ />
65
+ {/* Animated stroke trace with gradient */}
66
+ <motion.path
67
+ d={ALPHA_PATH}
68
+ fill="none"
69
+ stroke="url(#stroke_gradient)"
70
+ strokeWidth="0.4"
71
+ strokeLinecap="round"
72
+ strokeLinejoin="round"
73
+ initial={{ strokeDashoffset: PATH_LENGTH }}
74
+ animate={{ strokeDashoffset: -PATH_LENGTH }}
75
+ style={{ strokeDasharray: `8 ${PATH_LENGTH - 8}` }}
76
+ transition={{
77
+ duration: 2,
78
+ repeat: Infinity,
79
+ repeatDelay: 8,
80
+ ease: "linear",
81
+ }}
82
+ />
83
+ <defs>
84
+ <linearGradient
85
+ id="paint0_linear_10081_30848"
86
+ x1="-3.27254"
87
+ y1="4.99923"
88
+ x2="6.36738"
89
+ y2="-2.36962"
90
+ gradientUnits="userSpaceOnUse"
91
+ >
92
+ <stop stopColor="#8AEFDB" />
93
+ <stop offset="0.5" stopColor="#D4BACE" />
94
+ <stop offset="1" stopColor="#CBE0F1" />
95
+ </linearGradient>
96
+ <linearGradient
97
+ id="stroke_gradient"
98
+ x1="0"
99
+ y1="0"
100
+ x2="12"
101
+ y2="10"
102
+ gradientUnits="userSpaceOnUse"
103
+ >
104
+ <stop stopColor="#8AEFDB" />
105
+ <stop offset="0.5" stopColor="#D4BACE" />
106
+ <stop offset="1" stopColor="#CBE0F1" />
107
+ </linearGradient>
108
+ </defs>
109
+ </svg>
110
+ );
111
+ };
112
+
113
+ // Define a Link component type that matches common routing patterns
114
+ export type LinkComponent = React.ComponentType<{
115
+ href: string;
116
+ className?: string;
117
+ "aria-label"?: string;
118
+ children: React.ReactNode;
119
+ onClick?: (e: React.MouseEvent) => void;
120
+ }>;
121
+
122
+ // Navigation link structure supporting nested links
123
+ export interface NavLinkItem {
124
+ label: string;
125
+ href?: string;
126
+ badge?: string;
127
+ bulletColor?: string;
128
+ children?: NavLinkItem[];
129
+ }
130
+
131
+ // Navigation section structure for tray content
132
+ export interface NavSection {
133
+ heading?: string;
134
+ headingGradient?: string;
135
+ columns?: NavColumn[];
136
+ }
137
+
138
+ export interface NavColumn {
139
+ title?: string;
140
+ links: NavLinkItem[];
141
+ }
142
+
143
+ // Main navigation item that can have nested content
144
+ export interface NavItem {
145
+ label: string;
146
+ href?: string;
147
+ section?: NavSection;
148
+ }
149
+
150
+ interface NavigationProps {
151
+ navLinks?: NavItem[];
152
+ logo?: React.ComponentType<{ className?: string; dark?: boolean }>;
153
+ userIcon?: React.ComponentType<{ className?: string }>;
154
+ beforeActions?: React.ReactNode; // Slot for custom items before user icon (e.g., language switcher)
155
+ userDropdown?: React.ReactNode; // Dropdown content to show when user icon is clicked
156
+ isStrategy?: boolean;
157
+ Link?: LinkComponent; // Optional Link component from routing library
158
+ onLinkClick?: (href: string) => void; // Fallback callback for navigation
159
+ onClose?: () => void;
160
+ }
161
+
162
+ export const StandardNavigation: React.FC<NavigationProps> = ({
163
+ navLinks = [],
164
+ logo: Logo,
165
+ userIcon: UserIcon,
166
+ beforeActions,
167
+ userDropdown,
168
+ isStrategy = false,
169
+ Link: LinkComponent,
170
+ onLinkClick,
171
+ onClose,
172
+ }) => {
173
+ const [isOpen, setIsOpen] = React.useState(false);
174
+ const [activeItem, setActiveItem] = React.useState<number | null>(null);
175
+ const [isUserDropdownOpen, setIsUserDropdownOpen] = React.useState(false);
176
+
177
+ const containerRef = React.useRef<HTMLElement>(null);
178
+ const menuRef = React.useRef<HTMLDivElement>(null);
179
+ const menuItemsRef = React.useRef<HTMLDivElement>(null);
180
+ const overlayRef = React.useRef<HTMLDivElement>(null);
181
+ const userDropdownRef = React.useRef<HTMLDivElement>(null);
182
+
183
+ const handleMouseLeave = () => {
184
+ setIsOpen(false);
185
+ setActiveItem(null);
186
+ };
187
+
188
+ const handleNavItemMouseEnter = (index: number) => {
189
+ const item = navLinks[index];
190
+ if (item?.section) {
191
+ setIsOpen(true);
192
+ setActiveItem(index + 1);
193
+ }
194
+ };
195
+
196
+ const handleUserIconClick = (e: React.MouseEvent) => {
197
+ e.preventDefault();
198
+ if (userDropdown) {
199
+ setIsUserDropdownOpen(!isUserDropdownOpen);
200
+ }
201
+ };
202
+
203
+ // Close user dropdown when clicking outside
204
+ React.useEffect(() => {
205
+ const handleClickOutside = (event: MouseEvent) => {
206
+ const target = event.target as Node;
207
+ if (
208
+ userDropdownRef.current &&
209
+ !userDropdownRef.current.contains(target)
210
+ ) {
211
+ setIsUserDropdownOpen(false);
212
+ }
213
+ };
214
+
215
+ if (isUserDropdownOpen) {
216
+ // Use setTimeout to avoid immediate closure on the click that opened it
217
+ setTimeout(() => {
218
+ document.addEventListener("mousedown", handleClickOutside);
219
+ }, 0);
220
+ return () => {
221
+ document.removeEventListener("mousedown", handleClickOutside);
222
+ };
223
+ }
224
+ }, [isUserDropdownOpen]);
225
+
226
+ // Internal NavLink component that handles different routing scenarios
227
+ const NavLink: React.FC<{
228
+ href: string;
229
+ className?: string;
230
+ "aria-label"?: string;
231
+ children: React.ReactNode;
232
+ }> = ({ href, className, "aria-label": ariaLabel, children }) => {
233
+ // If Link component is provided, use it
234
+ if (LinkComponent) {
235
+ return (
236
+ <LinkComponent href={href} className={className} aria-label={ariaLabel}>
237
+ {children}
238
+ </LinkComponent>
239
+ );
240
+ }
241
+
242
+ // If onLinkClick callback is provided, use anchor with click handler
243
+ if (onLinkClick) {
244
+ return (
245
+ <a
246
+ href={href}
247
+ className={className}
248
+ aria-label={ariaLabel}
249
+ onClick={(e) => {
250
+ e.preventDefault();
251
+ onLinkClick(href);
252
+ }}
253
+ >
254
+ {children}
255
+ </a>
256
+ );
257
+ }
258
+
259
+ // Default: plain anchor tag (browser navigation)
260
+ return (
261
+ <a href={href} className={className} aria-label={ariaLabel}>
262
+ {children}
263
+ </a>
264
+ );
265
+ };
266
+
267
+ // Render a navigation link item
268
+ const renderNavLinkItem = (link: NavLinkItem) => {
269
+ const href = link.href || "#";
270
+ const hasBullet = link.bulletColor !== undefined;
271
+ const hasBadge = !!link.badge;
272
+ const isSmall = hasBullet || link.children;
273
+
274
+ if (hasBullet) {
275
+ return (
276
+ <div className="navigation-tray-bullet-item">
277
+ {link.bulletColor && link.bulletColor !== "" ? (
278
+ <NavBullet color={link.bulletColor} />
279
+ ) : (
280
+ <div className="navigation-tray-bullet" />
281
+ )}
282
+ <NavLink
283
+ href={href}
284
+ className="navigation-tray-link-small"
285
+ >
286
+ {link.label}
287
+ </NavLink>
288
+ </div>
289
+ );
290
+ }
291
+
292
+ if (hasBadge) {
293
+ return (
294
+ <NavLink
295
+ href={href}
296
+ className="navigation-tray-link navigation-tray-link--with-badge"
297
+ >
298
+ {link.label}
299
+ <span className="navigation-tray-badge">{link.badge}</span>
300
+ </NavLink>
301
+ );
302
+ }
303
+
304
+ return (
305
+ <NavLink
306
+ href={href}
307
+ className={isSmall ? "navigation-tray-link-small" : "navigation-tray-link"}
308
+ >
309
+ {link.label}
310
+ </NavLink>
311
+ );
312
+ };
313
+
314
+ // Render a navigation column
315
+ const renderNavColumn = (column: NavColumn) => {
316
+ if (!column.links || column.links.length === 0) return null;
317
+
318
+ const hasSpacing = column.links.some(
319
+ (link) => !link.bulletColor && !link.children,
320
+ );
321
+
322
+ return (
323
+ <div key={column.title || "column"} className="navigation-tray-column">
324
+ {column.title && (
325
+ <h3 className="navigation-tray-column-title">{column.title}</h3>
326
+ )}
327
+ <ul
328
+ className={cn(
329
+ "navigation-tray-column-list",
330
+ hasSpacing && "navigation-tray-column-list--spacing-5",
331
+ )}
332
+ >
333
+ {column.links.map((link, idx) => (
334
+ <li key={idx}>{renderNavLinkItem(link)}</li>
335
+ ))}
336
+ </ul>
337
+ </div>
338
+ );
339
+ };
340
+
341
+ // Render the active tray section
342
+ const renderTraySection = () => {
343
+ if (!activeItem) return null;
344
+
345
+ const activeNavItem = navLinks[activeItem - 1];
346
+ if (!activeNavItem?.section) return null;
347
+
348
+ const section = activeNavItem.section;
349
+
350
+ return (
351
+ <div className="navigation-tray-section">
352
+ {section.heading && (
353
+ <h2
354
+ className="navigation-tray-heading"
355
+ style={
356
+ section.headingGradient
357
+ ? {
358
+ background: section.headingGradient,
359
+ WebkitBackgroundClip: "text",
360
+ WebkitTextFillColor: "transparent",
361
+ backgroundClip: "text",
362
+ }
363
+ : undefined
364
+ }
365
+ >
366
+ {section.heading}
367
+ </h2>
368
+ )}
369
+
370
+ {section.columns && section.columns.length > 0 && (
371
+ <div className="navigation-tray-columns">
372
+ {section.columns.map((column, idx) => renderNavColumn(column))}
373
+ </div>
374
+ )}
375
+ </div>
376
+ );
377
+ };
378
+
379
+ return (
380
+ <nav
381
+ ref={containerRef}
382
+ className={cn("navigation", isStrategy && "navigation--strategy")}
383
+ data-strategy={isStrategy}
384
+ onMouseLeave={handleMouseLeave}
385
+ >
386
+ {!isStrategy && (
387
+ <div
388
+ ref={overlayRef}
389
+ className="navigation-overlay"
390
+ data-open={isOpen}
391
+ />
392
+ )}
393
+
394
+ <div className="navigation-container">
395
+ <div className="navigation-logo-container">
396
+ {isStrategy ? (
397
+ <Button
398
+ className="navigation-close-button"
399
+ size="icon"
400
+ onClick={(e) => {
401
+ e.preventDefault();
402
+ if (onClose) {
403
+ onClose();
404
+ } else if (onLinkClick) {
405
+ onLinkClick("/");
406
+ }
407
+ }}
408
+ >
409
+ <UtilityClose className="navigation-close-icon" />
410
+ </Button>
411
+ ) : null}
412
+
413
+ <NavLink
414
+ href="/"
415
+ className={cn(
416
+ "navigation-logo-link",
417
+ isStrategy && "navigation-logo-link--strategy",
418
+ )}
419
+ data-strategy={isStrategy}
420
+ aria-label="Home"
421
+ >
422
+ {Logo ? (
423
+ <>
424
+
425
+ <NavLogo dark={isStrategy} />
426
+ <StandardLogo className={cn(!isStrategy ? "text-white" : "text-black")} />
427
+ <AlphaIcon dark={isStrategy} />
428
+ </>
429
+
430
+ ) : null}
431
+ </NavLink>
432
+ </div>
433
+
434
+ {!isStrategy ? (
435
+ <div className="navigation-nav-container">
436
+ <ul className="navigation-nav-list">
437
+ {navLinks.map((item, index) => (
438
+ <li
439
+ key={index}
440
+ className="navigation-nav-item"
441
+ onMouseEnter={() => handleNavItemMouseEnter(index)}
442
+ >
443
+ {item.href ? (
444
+ <NavLink href={item.href} className="navigation-nav-link">
445
+ {item.label}
446
+ </NavLink>
447
+ ) : (
448
+ <span className="navigation-nav-link">{item.label}</span>
449
+ )}
450
+
451
+ {/* Underline */}
452
+ {activeItem === index + 1 && (
453
+ <motion.div
454
+ id="underline"
455
+ className="navigation-underline"
456
+ layoutId="underline"
457
+ transition={{ duration: 0.2, ease: "easeOut" }}
458
+ >
459
+ <div
460
+ className="navigation-underline-circle navigation-underline-circle--gradient"
461
+ style={{
462
+ width: "10px",
463
+ height: "5px",
464
+ borderBottomLeftRadius: "100px",
465
+ borderBottomRightRadius: "100px",
466
+ borderBottom: "0",
467
+ boxSizing: "border-box",
468
+ }}
469
+ />
470
+ </motion.div>
471
+ )}
472
+ {index === 0 && (
473
+ <div
474
+ className="navigation-underline navigation-underline--static"
475
+ style={{ transition: "all 0.2s ease-out" }}
476
+ >
477
+ <div
478
+ className="navigation-underline-circle navigation-underline-circle--white"
479
+ style={{
480
+ width: "10px",
481
+ height: "5px",
482
+ borderTopLeftRadius: "100px",
483
+ borderTopRightRadius: "100px",
484
+ borderBottom: "0",
485
+ boxSizing: "border-box",
486
+ }}
487
+ />
488
+ </div>
489
+ )}
490
+ </li>
491
+ ))}
492
+ </ul>
493
+ </div>
494
+ ) : null}
495
+
496
+ <div className="navigation-actions">
497
+ {beforeActions && (
498
+ <div className="navigation-actions-before">{beforeActions}</div>
499
+ )}
500
+ <div className="navigation-account-wrapper" ref={userDropdownRef}>
501
+ {userDropdown ? (
502
+ <button
503
+ type="button"
504
+ aria-label="Account menu"
505
+ aria-expanded={isUserDropdownOpen}
506
+ className={cn(
507
+ "navigation-account-link",
508
+ isUserDropdownOpen && "navigation-account-link--open",
509
+ )}
510
+ data-open={isOpen}
511
+ data-strategy={isStrategy}
512
+ onClick={handleUserIconClick}
513
+ >
514
+ {UserIcon ? (
515
+ <UserIcon
516
+ className={cn(
517
+ "navigation-account-icon",
518
+ isStrategy
519
+ ? "navigation-account-icon--strategy"
520
+ : "navigation-account-icon--default",
521
+ )}
522
+ />
523
+ ) : (
524
+ <div className="navigation-account-icon-placeholder">User</div>
525
+ )}
526
+ </button>
527
+ ) : (
528
+ <NavLink
529
+ href="/"
530
+ aria-label="Account link"
531
+ className="navigation-account-link"
532
+ data-open={isOpen}
533
+ data-strategy={isStrategy}
534
+ >
535
+ {UserIcon ? (
536
+ <UserIcon
537
+ className={cn(
538
+ "navigation-account-icon",
539
+ isStrategy
540
+ ? "navigation-account-icon--strategy"
541
+ : "navigation-account-icon--default",
542
+ )}
543
+ />
544
+ ) : (
545
+ <div className="navigation-account-icon-placeholder">User</div>
546
+ )}
547
+ </NavLink>
548
+ )}
549
+ {isUserDropdownOpen && userDropdown && (
550
+ <div className="navigation-user-dropdown">{userDropdown}</div>
551
+ )}
552
+ </div>
553
+ </div>
554
+ </div>
555
+
556
+ {isOpen && <div className="navigation-divider" />}
557
+
558
+ {/* Tray */}
559
+ <div ref={menuRef} className="navigation-tray" data-open={isOpen}>
560
+ <div className="navigation-tray-content">
561
+ <div ref={menuItemsRef} className="navigation-tray-grid">
562
+ <AnimatePresence mode="wait">
563
+ <motion.div
564
+ key={activeItem ? activeItem : "empty"}
565
+ initial={{ y: 10, opacity: 0 }}
566
+ animate={{ y: 0, opacity: 1 }}
567
+ exit={{ y: -10, opacity: 0 }}
568
+ transition={{ duration: 0.2 }}
569
+ >
570
+ {renderTraySection()}
571
+ </motion.div>
572
+ </AnimatePresence>
573
+ </div>
574
+ </div>
575
+ </div>
576
+ </nav>
577
+ );
578
+ };
package/client/global.css CHANGED
@@ -6088,6 +6088,7 @@ body {
6088
6088
  align-items: center;
6089
6089
  transition: transform 300ms ease-in-out;
6090
6090
  will-change: transform;
6091
+ gap: 6px;
6091
6092
  }
6092
6093
 
6093
6094
  .navigation-logo-link[data-strategy="true"] {