Skip to content

Commit 23fef7f

Browse files
authored
Add animations to the buttons (#12)
1 parent fc62b45 commit 23fef7f

8 files changed

Lines changed: 120 additions & 31 deletions

File tree

src/lib/components/Button.tsx

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { clsx } from "@lib/utils/clsx";
22
import { WithRequired } from "@lib/utils/typeUtils/WithRequired";
3-
import { HTMLMotionProps, motion } from "framer-motion";
4-
import { forwardRef } from "react";
3+
import { HTMLMotionProps, motion, useAnimate } from "framer-motion";
4+
import { forwardRef, useImperativeHandle, useMemo, useRef } from "react";
55

66
type ButtonStyleProps = {
77
variant?: "solid" | "outline" | "link";
@@ -33,23 +33,60 @@ function variantClasses({
3333
}
3434
}
3535

36-
function commonProps<Type extends "a" | "button">({
37-
className,
38-
...props
39-
}: Type extends "a" ? LinkProps : ButtonProps): (Type extends "a"
40-
? LinkProps
41-
: ButtonProps) & { className: string } {
42-
// @ts-expect-error These types do match, but TypeScript doesn't seem to like it
43-
return {
44-
className: clsx(variantClasses(props), className, "cursor-pointer"),
45-
...props,
36+
function mergeRefs<T>(
37+
...refs: (React.Ref<T> | undefined)[]
38+
): React.RefCallback<T> {
39+
return (value) => {
40+
refs.forEach((ref) => {
41+
if (typeof ref === "function") {
42+
ref(value);
43+
} else if (ref !== null) {
44+
(ref as React.MutableRefObject<T | null>).current = value;
45+
}
46+
});
4647
};
4748
}
4849

50+
function useCommonProps<Type extends "a" | "button">(
51+
{ className, ...props }: Type extends "a" ? LinkProps : ButtonProps,
52+
ref: Type extends "a"
53+
? React.Ref<HTMLAnchorElement>
54+
: React.Ref<HTMLButtonElement>,
55+
): (Type extends "a" ? LinkProps : ButtonProps) & { className: string } {
56+
const [scope, animate] = useAnimate();
57+
58+
// @ts-expect-error These types do match, but TypeScript doesn't seem to like it
59+
return useMemo(
60+
() => ({
61+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
62+
onClick: async (e: any) => {
63+
props.onClick?.(e);
64+
await animate(
65+
scope.current,
66+
{ backgroundColor: "#FF00A6" },
67+
{ duration: 0.1 },
68+
);
69+
await animate(
70+
scope.current,
71+
{ backgroundColor: "#DB0082" },
72+
{ duration: 0.1 },
73+
);
74+
},
75+
initial: { backgroundColor: "#DB0082" },
76+
whileHover: { backgroundColor: "#AC0067" },
77+
whileFocus: { backgroundColor: "#AC0067" },
78+
className: clsx(variantClasses(props), className, "cursor-pointer"),
79+
...props,
80+
ref: mergeRefs(ref, scope),
81+
}),
82+
[animate, className, props, ref, scope],
83+
);
84+
}
85+
4986
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
5087
({ children, ...props }, ref) => {
5188
return (
52-
<motion.button ref={ref} {...commonProps<"button">(props)}>
89+
<motion.button {...useCommonProps<"button">(props, ref)}>
5390
{children}
5491
</motion.button>
5592
);
@@ -59,8 +96,10 @@ Button.displayName = "Button";
5996

6097
export const ButtonLink = forwardRef<HTMLAnchorElement, LinkProps>(
6198
({ children, ...props }, ref) => {
99+
const common = useCommonProps<"a">(props);
100+
62101
return (
63-
<motion.a ref={ref} {...commonProps<"a">(props)}>
102+
<motion.a ref={ref} {...common}>
64103
{children}
65104
</motion.a>
66105
);
@@ -71,7 +110,10 @@ ButtonLink.displayName = "ButtonLink";
71110
export const Link = forwardRef<HTMLAnchorElement, Omit<LinkProps, "variant">>(
72111
({ children, ...props }, ref) => {
73112
return (
74-
<motion.a ref={ref} {...commonProps<"a">({ variant: "link", ...props })}>
113+
<motion.a
114+
ref={ref}
115+
{...useCommonProps<"a">({ variant: "link", ...props })}
116+
>
75117
{children}
76118
</motion.a>
77119
);

src/lib/integrations/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Github } from "./github";
22
import { GitLab } from "./gitlab";
3+
import { Preview } from "./preview";
34

4-
const INTEGRATIONS = [Github, GitLab] as const;
5+
const INTEGRATIONS = [Github, GitLab, Preview] as const;
56

67
export function getSupportedIntegration(url: string | URL) {
78
return INTEGRATIONS.find((integration) => integration.supports(url));

src/lib/integrations/preview.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Integration } from "./core";
2+
3+
/** A dummy integration just for testing and development.
4+
*
5+
* This is always supported on every page on dev, and never on production.
6+
*/
7+
export const Preview: Integration = {
8+
platform: "Github",
9+
supports() {
10+
return NODE_ENV === "development";
11+
},
12+
getButtonTarget(document: Document) {
13+
return document.body;
14+
},
15+
getRepo({ url }) {
16+
const u = new URL(url);
17+
if (u.searchParams.has("repo")) {
18+
return u.searchParams.get("repo")!;
19+
}
20+
return "github/choosealicense.com";
21+
},
22+
getBranch({ url }) {
23+
const u = new URL(url);
24+
if (u.searchParams.has("branch")) {
25+
return u.searchParams.get("branch")!;
26+
}
27+
return "spdx-license-templates";
28+
},
29+
};

src/pages/PreviewContent.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Button } from "@lib/components/Button";
2+
import { CloneButton } from "./content/CloneButton";
3+
4+
export function Preview() {
5+
return (
6+
<div className="w-full min-h-svh top-0 absolute flex justify-center items-center bg-slate-200">
7+
<div className="m-4 bg-white p-8 rounded flex flex-col gap-4">
8+
<CloneButton portal={document.body} />
9+
<Button>Test</Button>
10+
</div>
11+
</div>
12+
);
13+
}

src/pages/preview.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
1-
import { attachShadow } from "@lib/utils/dom/shadow";
21
import { createRoot } from "react-dom/client";
3-
import { CloneButton } from "./content/CloneButton";
2+
import "../tailwind.css";
3+
import { Preview } from "./PreviewContent";
44

5-
function init() {
6-
const { shadow: rootContainer } = attachShadow(
7-
document.getElementById("root"),
8-
);
9-
const { shadow: portalContainer } = attachShadow(document.body);
10-
const root = createRoot(rootContainer);
11-
12-
root.render(<CloneButton portal={portalContainer} />);
13-
}
14-
15-
init();
5+
const root = createRoot(document.body);
6+
root.render(<Preview />);

src/vite-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
declare const APP_VERSION: string;
44
declare const PREVIEW: boolean;
5+
declare const NODE_ENV: "development" | "production";

tailwind.config.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@ module.exports = {
77
DEFAULT: "1rem",
88
},
99
colors: {
10-
primary: "#ba50ff",
11-
"primary-contrast": "#fff",
10+
primary: {
11+
DEFAULT: "#DB0082", // Set 500 as the default primary color
12+
100: "#FFD5EE",
13+
200: "#FF97D5",
14+
300: "#FF59BD",
15+
400: "#FF1BA4",
16+
500: "#DB0082",
17+
600: "#AC0067",
18+
700: "#7D004B",
19+
800: "#4E002F",
20+
900: "#1F0013",
21+
contrast: "#fff",
22+
},
1223
text: "#000",
1324
background: "#fff",
1425
},

vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ export default defineConfig({
4242
define: {
4343
APP_VERSION: JSON.stringify(pkg.version),
4444
PREVIEW: JSON.stringify(process.env.PREVIEW !== '"false"'),
45+
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
4546
},
4647
});

0 commit comments

Comments
 (0)