@syscore/ui-library 1.15.3 → 1.16.0
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/client/components/icons/UtilityCompare.tsx +37 -35
- package/client/components/icons/UtilityFeedback.tsx +30 -0
- package/client/components/icons/UtilityRevisionsHide.tsx +1 -1
- package/client/components/icons/UtilityRevisionsShow.tsx +40 -0
- package/client/components/icons/imperative-badges/BadgeImperativePrimary.tsx +45 -0
- package/client/components/icons/imperative-badges/BadgeImperativeSecondary.tsx +53 -0
- package/client/components/icons/imperative-badges/index.tsx +2 -0
- package/client/components/ui/mobile-nav.tsx +278 -0
- package/client/components/ui/tooltip.tsx +39 -20
- package/client/global.css +104 -7
- package/client/ui/Icons.stories.tsx +52 -30
- package/client/ui/MobileNav.stories.tsx +317 -0
- package/client/ui/PageHeader.stories.tsx +82 -0
- package/client/ui/Tooltip.stories.tsx +38 -30
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +24 -0
- package/dist/index.es.js +449 -41
- package/package.json +1 -1
|
@@ -12,58 +12,60 @@ export const UtilityCompare: React.FC<UtilityCompareProps> = ({
|
|
|
12
12
|
<div
|
|
13
13
|
className={cn(
|
|
14
14
|
"size-4 flex items-center justify-center text-gray-500",
|
|
15
|
-
className
|
|
15
|
+
className,
|
|
16
16
|
)}
|
|
17
17
|
>
|
|
18
18
|
<svg
|
|
19
19
|
xmlns="http://www.w3.org/2000/svg"
|
|
20
|
-
width="
|
|
21
|
-
height="
|
|
22
|
-
viewBox="0 0
|
|
20
|
+
width="31"
|
|
21
|
+
height="34"
|
|
22
|
+
viewBox="0 0 31 34"
|
|
23
23
|
fill="none"
|
|
24
|
-
className={cn("
|
|
24
|
+
className={cn("text-inherit w-full h-full")}
|
|
25
25
|
>
|
|
26
26
|
<path
|
|
27
|
-
d="
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
d="M10.6002 5.79905C10.6002 8.44949 8.45114 10.5981 5.80011 10.5981C3.14908 10.5981 1 8.44949 1 5.79905C1 3.14861 3.14908 1 5.80011 1C8.45114 1 10.6002 3.14861 10.6002 5.79905Z"
|
|
28
|
+
fill="#E0FBF5"
|
|
29
|
+
stroke="#282A31"
|
|
30
|
+
stroke-width="2"
|
|
31
|
+
stroke-linecap="round"
|
|
32
|
+
stroke-linejoin="round"
|
|
32
33
|
/>
|
|
33
34
|
<path
|
|
34
|
-
d="
|
|
35
|
-
stroke="
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
d="M5.80078 16.998L5.80078 24.9965C5.80078 25.845 6.13793 26.6588 6.73806 27.2587C7.33819 27.8587 8.15214 28.1958 9.00086 28.1958L20.2011 28.1958"
|
|
36
|
+
stroke="#282A31"
|
|
37
|
+
stroke-width="2"
|
|
38
|
+
stroke-linecap="round"
|
|
39
|
+
stroke-linejoin="round"
|
|
39
40
|
/>
|
|
40
41
|
<path
|
|
41
|
-
d="
|
|
42
|
-
stroke="
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
d="M10.6002 21.7971L5.80011 16.998L1 21.7971"
|
|
43
|
+
stroke="#282A31"
|
|
44
|
+
stroke-width="2"
|
|
45
|
+
stroke-linecap="round"
|
|
46
|
+
stroke-linejoin="round"
|
|
46
47
|
/>
|
|
47
48
|
<path
|
|
48
|
-
d="
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
d="M29.8001 28.2014C29.8001 30.8518 27.651 33.0004 24.9999 33.0004C22.3489 33.0004 20.1998 30.8518 20.1998 28.2014C20.1998 25.551 22.3489 23.4023 24.9999 23.4023C27.651 23.4023 29.8001 25.5509 29.8001 28.2014Z"
|
|
50
|
+
fill="#EFF5FB"
|
|
51
|
+
stroke="#282A31"
|
|
52
|
+
stroke-width="2"
|
|
53
|
+
stroke-linecap="round"
|
|
54
|
+
stroke-linejoin="round"
|
|
53
55
|
/>
|
|
54
56
|
<path
|
|
55
|
-
d="
|
|
56
|
-
stroke="
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
d="M25.0002 16.9946V8.99624C25.0002 8.14771 24.663 7.33394 24.0629 6.73395C23.4628 6.13395 22.6488 5.79687 21.8001 5.79687L10.5999 5.79688"
|
|
58
|
+
stroke="#282A31"
|
|
59
|
+
stroke-width="2"
|
|
60
|
+
stroke-linecap="round"
|
|
61
|
+
stroke-linejoin="round"
|
|
60
62
|
/>
|
|
61
63
|
<path
|
|
62
|
-
d="
|
|
63
|
-
stroke="
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
d="M20.1998 12.2012L24.9999 17.0002L29.8001 12.2012"
|
|
65
|
+
stroke="#282A31"
|
|
66
|
+
stroke-width="2"
|
|
67
|
+
stroke-linecap="round"
|
|
68
|
+
stroke-linejoin="round"
|
|
67
69
|
/>
|
|
68
70
|
</svg>
|
|
69
71
|
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
|
|
3
|
+
export function UtilityFeedback({ className }: { className?: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
className={cn(
|
|
7
|
+
"size-4 flex items-center justify-center text-gray-500",
|
|
8
|
+
className,
|
|
9
|
+
)}
|
|
10
|
+
>
|
|
11
|
+
<svg
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
width="30"
|
|
14
|
+
height="28"
|
|
15
|
+
viewBox="0 0 30 28"
|
|
16
|
+
fill="none"
|
|
17
|
+
className={cn("text-inherit")}
|
|
18
|
+
>
|
|
19
|
+
<path
|
|
20
|
+
d="M29 20.162C29 20.888 28.705 21.5843 28.1799 22.0977C27.6548 22.611 26.9426 22.8995 26.2 22.8995H7.7592C7.01665 22.8996 6.30458 23.1881 5.7796 23.7015L2.6968 26.7154C2.55779 26.8513 2.38068 26.9438 2.18788 26.9813C1.99508 27.0188 1.79524 26.9996 1.61363 26.926C1.43202 26.8525 1.27678 26.728 1.16756 26.5682C1.05834 26.4084 1.00003 26.2205 1 26.0283V3.73743C1 3.01142 1.295 2.31514 1.8201 1.80178C2.3452 1.28841 3.05739 1 3.8 1H26.2C26.9426 1 27.6548 1.28841 28.1799 1.80178C28.705 2.31514 29 3.01142 29 3.73743V20.162Z"
|
|
21
|
+
fill="#D6F4FB"
|
|
22
|
+
stroke="#71747D"
|
|
23
|
+
stroke-width="2"
|
|
24
|
+
stroke-linecap="round"
|
|
25
|
+
stroke-linejoin="round"
|
|
26
|
+
/>
|
|
27
|
+
</svg>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -14,7 +14,7 @@ export function UtilityRevisionsHide({ className }: { className?: string }) {
|
|
|
14
14
|
height="15"
|
|
15
15
|
viewBox="0 0 17 15"
|
|
16
16
|
fill="none"
|
|
17
|
-
className={cn("
|
|
17
|
+
className={cn("text-inherit")}
|
|
18
18
|
>
|
|
19
19
|
<path
|
|
20
20
|
d="M5.48682 6.27734L0.75 11.0142V13.3826H7.85523L10.2236 11.0142"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
|
|
3
|
+
export function UtilityRevisionsShow({ className }: { className?: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
className={cn(
|
|
7
|
+
"size-4 flex items-center justify-center text-gray-500",
|
|
8
|
+
className,
|
|
9
|
+
)}
|
|
10
|
+
>
|
|
11
|
+
<svg
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
width="30"
|
|
14
|
+
height="26"
|
|
15
|
+
viewBox="0 0 30 26"
|
|
16
|
+
fill="none"
|
|
17
|
+
className={cn("text-inherit")}
|
|
18
|
+
>
|
|
19
|
+
<path
|
|
20
|
+
d="M9.84194 11.5L1 20.5006V25.0009H14.2629L18.6839 20.5006"
|
|
21
|
+
fill="#F6EFD0"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
d="M9.84194 11.5L1 20.5006V25.0009H14.2629L18.6839 20.5006"
|
|
25
|
+
stroke="#282A31"
|
|
26
|
+
stroke-width="2"
|
|
27
|
+
stroke-linecap="round"
|
|
28
|
+
stroke-linejoin="round"
|
|
29
|
+
/>
|
|
30
|
+
<path
|
|
31
|
+
d="M28.9999 13.0008L22.2211 19.9013C21.6702 20.451 20.9295 20.7589 20.158 20.7589C19.3865 20.7589 18.6458 20.451 18.0949 19.9013L10.4319 12.1008C9.89184 11.5399 9.58936 10.7859 9.58936 10.0006C9.58936 9.21531 9.89184 8.4613 10.4319 7.90048L17.2107 1"
|
|
32
|
+
stroke="#282A31"
|
|
33
|
+
stroke-width="2"
|
|
34
|
+
stroke-linecap="round"
|
|
35
|
+
stroke-linejoin="round"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
export const BadgeImperativePrimary = ({
|
|
4
|
+
className,
|
|
5
|
+
}: {
|
|
6
|
+
className?: string;
|
|
7
|
+
}) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className={cn("size-6 flex items-center justify-center", className)}>
|
|
10
|
+
<svg
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
width="20"
|
|
13
|
+
height="22"
|
|
14
|
+
viewBox="0 0 20 22"
|
|
15
|
+
fill="none"
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
d="M19.2133 7.81921C19.2133 6.38498 18.4454 5.06051 17.2008 4.3479L11.5941 1.13788C10.3629 0.432974 8.85042 0.432974 7.61921 1.13788L2.01255 4.3479C0.767878 5.06051 0 6.38498 0 7.81921V14.1808C0 15.615 0.767878 16.9395 2.01255 17.6521L7.61921 20.8621C8.85042 21.567 10.3629 21.567 11.5941 20.8621L17.2008 17.6521C18.4454 16.9395 19.2133 15.615 19.2133 14.1808V7.81921Z"
|
|
19
|
+
fill="#905C84"
|
|
20
|
+
/>
|
|
21
|
+
<path
|
|
22
|
+
d="M9.68994 5.5V16.5023"
|
|
23
|
+
stroke="white"
|
|
24
|
+
stroke-width="1.5"
|
|
25
|
+
stroke-linecap="round"
|
|
26
|
+
stroke-linejoin="round"
|
|
27
|
+
/>
|
|
28
|
+
<path
|
|
29
|
+
d="M14.4542 8.25055L4.92627 13.7517"
|
|
30
|
+
stroke="white"
|
|
31
|
+
stroke-width="1.5"
|
|
32
|
+
stroke-linecap="round"
|
|
33
|
+
stroke-linejoin="round"
|
|
34
|
+
/>
|
|
35
|
+
<path
|
|
36
|
+
d="M4.92627 8.25055L14.4542 13.7517"
|
|
37
|
+
stroke="white"
|
|
38
|
+
stroke-width="1.5"
|
|
39
|
+
stroke-linecap="round"
|
|
40
|
+
stroke-linejoin="round"
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
export const BadgeImperativeSecondary = ({
|
|
4
|
+
className,
|
|
5
|
+
}: {
|
|
6
|
+
className?: string;
|
|
7
|
+
}) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className={cn("size-6 flex items-center justify-center", className)}>
|
|
10
|
+
<svg
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
width="20"
|
|
13
|
+
height="22"
|
|
14
|
+
viewBox="0 0 20 22"
|
|
15
|
+
fill="none"
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
d="M19.2133 7.81921C19.2133 6.38498 18.4454 5.06051 17.2008 4.3479L11.5941 1.13788C10.3629 0.432974 8.85042 0.432974 7.61921 1.13788L2.01255 4.3479C0.767878 5.06051 0 6.38498 0 7.81921V14.1808C0 15.615 0.767878 16.9395 2.01255 17.6521L7.61921 20.8621C8.85042 21.567 10.3629 21.567 11.5941 20.8621L17.2008 17.6521C18.4454 16.9395 19.2133 15.615 19.2133 14.1808V7.81921Z"
|
|
19
|
+
fill="#128BA6"
|
|
20
|
+
fill-opacity="0.08"
|
|
21
|
+
/>
|
|
22
|
+
<path
|
|
23
|
+
d="M7.86719 1.57227C8.9445 0.955469 10.2684 0.955469 11.3457 1.57227L16.9521 4.78223C18.0412 5.40577 18.7129 6.56439 18.7129 7.81934V14.1807C18.7129 15.4356 18.0412 16.5942 16.9521 17.2178L11.3457 20.4277C10.2684 21.0445 8.9445 21.0445 7.86719 20.4277L2.26074 17.2178C1.17187 16.5942 0.5 15.4355 0.5 14.1807V7.81934C0.5 6.56452 1.17187 5.40582 2.26074 4.78223L7.86719 1.57227Z"
|
|
24
|
+
stroke="#128BA6"
|
|
25
|
+
stroke-opacity="0.16"
|
|
26
|
+
stroke-linecap="round"
|
|
27
|
+
stroke-linejoin="round"
|
|
28
|
+
/>
|
|
29
|
+
<path
|
|
30
|
+
d="M9.68994 5.5V16.5023"
|
|
31
|
+
stroke="#0F748A"
|
|
32
|
+
stroke-width="1.5"
|
|
33
|
+
stroke-linecap="round"
|
|
34
|
+
stroke-linejoin="round"
|
|
35
|
+
/>
|
|
36
|
+
<path
|
|
37
|
+
d="M14.4542 8.25055L4.92627 13.7517"
|
|
38
|
+
stroke="#0F748A"
|
|
39
|
+
stroke-width="1.5"
|
|
40
|
+
stroke-linecap="round"
|
|
41
|
+
stroke-linejoin="round"
|
|
42
|
+
/>
|
|
43
|
+
<path
|
|
44
|
+
d="M4.92627 8.25055L14.4542 13.7517"
|
|
45
|
+
stroke="#0F748A"
|
|
46
|
+
stroke-width="1.5"
|
|
47
|
+
stroke-linecap="round"
|
|
48
|
+
stroke-linejoin="round"
|
|
49
|
+
/>
|
|
50
|
+
</svg>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo,
|
|
6
|
+
useEffect,
|
|
7
|
+
ReactNode,
|
|
8
|
+
createContext,
|
|
9
|
+
useContext,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { motion, useMotionValue, animate, type PanInfo } from "motion/react";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
|
|
14
|
+
interface MobileNavContextValue {
|
|
15
|
+
activeKey: string | null;
|
|
16
|
+
open: boolean;
|
|
17
|
+
toggle: (key: string) => void;
|
|
18
|
+
close: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MobileNavProps {
|
|
22
|
+
className?: string;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MobileNavPanelProps {
|
|
27
|
+
className?: string;
|
|
28
|
+
children: (activeKey: string) => ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface MobileNavBarProps {
|
|
32
|
+
className?: string;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface MobileNavTriggerProps {
|
|
37
|
+
value: string;
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
label: string;
|
|
40
|
+
/** If provided, fires this action instead of toggling the panel */
|
|
41
|
+
onAction?: () => void;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SPRING_CONFIG = { stiffness: 300, damping: 30 };
|
|
46
|
+
|
|
47
|
+
const MobileNavContext = createContext<MobileNavContextValue | null>(null);
|
|
48
|
+
|
|
49
|
+
const useMobileNav = () => {
|
|
50
|
+
const ctx = useContext(MobileNavContext);
|
|
51
|
+
if (!ctx)
|
|
52
|
+
throw new Error(
|
|
53
|
+
"MobileNav compound components must be used within <MobileNav>",
|
|
54
|
+
);
|
|
55
|
+
return ctx;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const MobileNav = ({ className, children }: MobileNavProps) => {
|
|
59
|
+
const [activeKey, setActiveKey] = useState<string | null>(null);
|
|
60
|
+
const [open, setOpen] = useState(false);
|
|
61
|
+
|
|
62
|
+
const toggle = useCallback(
|
|
63
|
+
(key: string) => {
|
|
64
|
+
if (activeKey === key && open) {
|
|
65
|
+
setOpen(false);
|
|
66
|
+
setActiveKey(null);
|
|
67
|
+
} else {
|
|
68
|
+
setActiveKey(key);
|
|
69
|
+
setOpen(true);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[activeKey, open],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const close = useCallback(() => {
|
|
76
|
+
setOpen(false);
|
|
77
|
+
setActiveKey(null);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const value = useMemo(
|
|
81
|
+
() => ({ activeKey, open, toggle, close }),
|
|
82
|
+
[activeKey, open, toggle, close],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<MobileNavContext.Provider value={value}>
|
|
87
|
+
<div className={cn("mobile-nav", className)}>{children}</div>
|
|
88
|
+
</MobileNavContext.Provider>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
|
|
93
|
+
const { activeKey, open, close } = useMobileNav();
|
|
94
|
+
|
|
95
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
96
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
97
|
+
|
|
98
|
+
const heightMV = useMotionValue(0);
|
|
99
|
+
const stopAnimation = useRef<(() => void) | null>(null);
|
|
100
|
+
|
|
101
|
+
// Track which state the panel is in: "closed" | "initial" | "full"
|
|
102
|
+
const stateRef = useRef<"closed" | "initial" | "full">("closed");
|
|
103
|
+
|
|
104
|
+
// Cache the initial content height so returning from full → initial is consistent
|
|
105
|
+
const cachedInitialHeight = useRef(0);
|
|
106
|
+
|
|
107
|
+
const getNavBarHeight = () => {
|
|
108
|
+
const nav = panelRef.current?.nextElementSibling as HTMLElement | null;
|
|
109
|
+
return nav?.offsetHeight ?? 0;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const getAvailableHeight = () => {
|
|
113
|
+
const parent = panelRef.current?.parentElement;
|
|
114
|
+
const container = parent?.clientHeight ?? window.innerHeight;
|
|
115
|
+
return container - getNavBarHeight();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const measureContentHeight = () => {
|
|
119
|
+
const content = contentRef.current;
|
|
120
|
+
if (!content) return 0;
|
|
121
|
+
// Measure the first child to get true content height without triggering reflow
|
|
122
|
+
const child = content.firstElementChild as HTMLElement | null;
|
|
123
|
+
const raw = (child?.offsetHeight ?? content.scrollHeight) + 40; // +40 for handle area
|
|
124
|
+
const max = getAvailableHeight() * 0.7;
|
|
125
|
+
return Math.min(raw, max);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const springTo = (target: number) => {
|
|
129
|
+
stopAnimation.current?.();
|
|
130
|
+
const controls = animate(heightMV, target, {
|
|
131
|
+
type: "spring",
|
|
132
|
+
...SPRING_CONFIG,
|
|
133
|
+
});
|
|
134
|
+
stopAnimation.current = () => controls.stop();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Animate open/close
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
let cancelled = false;
|
|
140
|
+
if (open) {
|
|
141
|
+
stateRef.current = "initial";
|
|
142
|
+
requestAnimationFrame(() => {
|
|
143
|
+
if (cancelled) return;
|
|
144
|
+
const h = measureContentHeight();
|
|
145
|
+
cachedInitialHeight.current = h;
|
|
146
|
+
springTo(h);
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
stateRef.current = "closed";
|
|
150
|
+
cachedInitialHeight.current = 0;
|
|
151
|
+
springTo(0);
|
|
152
|
+
}
|
|
153
|
+
return () => {
|
|
154
|
+
cancelled = true;
|
|
155
|
+
};
|
|
156
|
+
}, [open]);
|
|
157
|
+
|
|
158
|
+
// Re-measure when content changes (e.g. switching tabs)
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!open) return;
|
|
161
|
+
let cancelled = false;
|
|
162
|
+
// Double rAF ensures new children have rendered before measuring
|
|
163
|
+
requestAnimationFrame(() => {
|
|
164
|
+
requestAnimationFrame(() => {
|
|
165
|
+
if (cancelled) return;
|
|
166
|
+
const h = measureContentHeight();
|
|
167
|
+
cachedInitialHeight.current = h;
|
|
168
|
+
if (stateRef.current === "initial" || stateRef.current === "closed") {
|
|
169
|
+
stateRef.current = "initial";
|
|
170
|
+
springTo(h);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
return () => {
|
|
175
|
+
cancelled = true;
|
|
176
|
+
};
|
|
177
|
+
}, [activeKey]);
|
|
178
|
+
|
|
179
|
+
// Track drag direction via ref for reliable gesture detection
|
|
180
|
+
const dragDirection = useRef<"up" | "down" | null>(null);
|
|
181
|
+
|
|
182
|
+
const handlePanStart = () => {
|
|
183
|
+
dragDirection.current = null;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handlePan = (_: unknown, info: PanInfo) => {
|
|
187
|
+
// Continuously track the latest direction during drag
|
|
188
|
+
dragDirection.current = info.offset.y < 0 ? "up" : "down";
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handlePanEnd = () => {
|
|
192
|
+
const dir = dragDirection.current;
|
|
193
|
+
if (!dir) return;
|
|
194
|
+
|
|
195
|
+
if (dir === "up") {
|
|
196
|
+
if (stateRef.current === "initial") {
|
|
197
|
+
stateRef.current = "full";
|
|
198
|
+
springTo(getAvailableHeight());
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
// dir === "down"
|
|
202
|
+
if (stateRef.current === "full") {
|
|
203
|
+
stateRef.current = "initial";
|
|
204
|
+
springTo(cachedInitialHeight.current);
|
|
205
|
+
} else if (stateRef.current === "initial") {
|
|
206
|
+
stateRef.current = "closed";
|
|
207
|
+
springTo(0);
|
|
208
|
+
close();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<motion.div
|
|
215
|
+
ref={panelRef}
|
|
216
|
+
className={cn("mobile-nav-panel", className)}
|
|
217
|
+
data-closed={!open || undefined}
|
|
218
|
+
style={{ height: heightMV }}
|
|
219
|
+
>
|
|
220
|
+
<motion.div
|
|
221
|
+
className="mobile-nav-handle"
|
|
222
|
+
onPanStart={handlePanStart}
|
|
223
|
+
onPan={handlePan}
|
|
224
|
+
onPanEnd={handlePanEnd}
|
|
225
|
+
>
|
|
226
|
+
<div className="mobile-nav-handle-bar" />
|
|
227
|
+
</motion.div>
|
|
228
|
+
|
|
229
|
+
<div ref={contentRef} className="mobile-nav-content">
|
|
230
|
+
{activeKey ? children(activeKey) : null}
|
|
231
|
+
</div>
|
|
232
|
+
</motion.div>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const MobileNavBar = ({ className, children }: MobileNavBarProps) => (
|
|
237
|
+
<nav className={cn("mobile-nav-bar", className)}>
|
|
238
|
+
<div className="mobile-nav-bar-inner">{children}</div>
|
|
239
|
+
</nav>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const MobileNavTrigger = ({
|
|
243
|
+
value,
|
|
244
|
+
children,
|
|
245
|
+
label,
|
|
246
|
+
onAction,
|
|
247
|
+
className,
|
|
248
|
+
}: MobileNavTriggerProps) => {
|
|
249
|
+
const { activeKey, open, toggle, close } = useMobileNav();
|
|
250
|
+
|
|
251
|
+
const handleClick = () => {
|
|
252
|
+
if (onAction) {
|
|
253
|
+
if (open) close();
|
|
254
|
+
onAction();
|
|
255
|
+
} else {
|
|
256
|
+
toggle(value);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<button
|
|
262
|
+
onClick={handleClick}
|
|
263
|
+
data-active={activeKey === value || undefined}
|
|
264
|
+
className={cn("mobile-nav-trigger group", className)}
|
|
265
|
+
aria-label={label}
|
|
266
|
+
>
|
|
267
|
+
{children}
|
|
268
|
+
</button>
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export { MobileNav, MobileNavPanel, MobileNavBar, MobileNavTrigger };
|
|
273
|
+
export type {
|
|
274
|
+
MobileNavProps,
|
|
275
|
+
MobileNavPanelProps,
|
|
276
|
+
MobileNavBarProps,
|
|
277
|
+
MobileNavTriggerProps,
|
|
278
|
+
};
|
|
@@ -2,10 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
6
|
|
|
6
7
|
import { cn } from "@/lib/utils";
|
|
7
8
|
import { UtilityClose } from "@/components/icons/UtilityClose";
|
|
8
9
|
|
|
10
|
+
const tooltipContentVariants = cva("tooltip-content", {
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: "tooltip-content--default",
|
|
14
|
+
simple: "tooltip-content--simple",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: "default",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
9
22
|
type TriggerMode = "hover" | "click";
|
|
10
23
|
|
|
11
24
|
// Context for passing trigger mode and close function to children
|
|
@@ -146,15 +159,17 @@ function TooltipTrigger({
|
|
|
146
159
|
|
|
147
160
|
function TooltipContent({
|
|
148
161
|
className,
|
|
162
|
+
variant,
|
|
149
163
|
sideOffset = 0,
|
|
150
164
|
side = "bottom",
|
|
151
165
|
children,
|
|
152
166
|
alignOffset = 0,
|
|
153
167
|
hideClose = false,
|
|
154
168
|
...props
|
|
155
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Content> &
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content> &
|
|
170
|
+
VariantProps<typeof tooltipContentVariants> & {
|
|
171
|
+
hideClose?: boolean;
|
|
172
|
+
}) {
|
|
158
173
|
const { trigger, close, triggerRef } = React.useContext(TooltipContext);
|
|
159
174
|
const contentRef = React.useRef<HTMLDivElement>(null);
|
|
160
175
|
|
|
@@ -193,25 +208,28 @@ function TooltipContent({
|
|
|
193
208
|
sideOffset={sideOffset}
|
|
194
209
|
alignOffset={alignOffset}
|
|
195
210
|
side={side}
|
|
196
|
-
className={cn(
|
|
211
|
+
className={cn(tooltipContentVariants({ variant }), className)}
|
|
197
212
|
{...props}
|
|
198
213
|
>
|
|
199
|
-
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
{variant !== "simple" && (
|
|
215
|
+
<div className="tooltip-arrow">
|
|
216
|
+
<svg
|
|
217
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
218
|
+
width="10"
|
|
219
|
+
height="5"
|
|
220
|
+
viewBox="0 0 10 5"
|
|
221
|
+
fill="none"
|
|
222
|
+
>
|
|
223
|
+
<path
|
|
224
|
+
d="M10 -1.74846e-06L-7.36867e-07 0C-3.29906e-07 2.76142 2.23858 5 5 5C7.76142 5 10 2.76142 10 -1.74846e-06Z"
|
|
225
|
+
fill="currentColor"
|
|
226
|
+
/>
|
|
227
|
+
</svg>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
{variant !== "simple" && !hideClose && (
|
|
231
|
+
<ToggleClose className="absolute top-4 right-4" />
|
|
232
|
+
)}
|
|
215
233
|
{children}
|
|
216
234
|
</TooltipPrimitive.Content>
|
|
217
235
|
</TooltipPrimitive.Portal>
|
|
@@ -236,6 +254,7 @@ export {
|
|
|
236
254
|
Tooltip,
|
|
237
255
|
TooltipTrigger,
|
|
238
256
|
TooltipContent,
|
|
257
|
+
tooltipContentVariants,
|
|
239
258
|
TooltipProvider,
|
|
240
259
|
ToggleClose,
|
|
241
260
|
};
|