Skip to content

Commit

Permalink
refactor: button component - improve props, variants, sizing, icons, … (
Browse files Browse the repository at this point in the history
#486)

* refactor: button component - improve props, variants, sizing, icons, customization, and Tailwind IntelliSense

* chore: removes duplicate transition-colors inside button component

* chore: removes duplicate shadow-sm inside button component

* refactor: add new button size "icon-md"

* chore: Update changeset

* Update .changeset/mighty-balloons-camp.md

Co-authored-by: Francisco Jiménez Aguilera <[email protected]>

* Update .changeset/mighty-balloons-camp.md

Co-authored-by: elessar.eth <[email protected]>

* feat: Add IconButton component and update usage in code

* refactor(web): encapsulate icon dimensions within the `IconButton` component

---------

Co-authored-by: Francisco Jiménez Aguilera <[email protected]>
Co-authored-by: elessar.eth <[email protected]>
  • Loading branch information
3 people authored Aug 26, 2024
1 parent 4875a48 commit be191ba
Show file tree
Hide file tree
Showing 17 changed files with 236 additions and 219 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-balloons-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/web": minor
---

Improved button component props, variants and sizing logic
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
{ "pattern": "tooling/*/" }
],
"tailwindCSS.experimental.configFile": "./tooling/tailwind/index.ts",
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@upstash/ratelimit": "^0.4.4",
"@vercel/analytics": "^1.0.2",
"@vercel/kv": "^0.2.3",
"class-variance-authority": "^0.7.0",
"classnames": "^2.3.2",
"echarts": "^5.4.2",
"echarts-for-react": "^3.0.2",
Expand All @@ -49,6 +50,7 @@
"react-dom": "18.2.0",
"react-loading-skeleton": "^3.3.1",
"superjson": "1.9.1",
"tailwind-merge": "^2.4.0",
"viem": "^2.17.4",
"zod": "^3.21.4"
},
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/AppLayout/BottomBarLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ReactElement } from "react";
import React from "react";

import { Button } from "~/components/Button";
import { ExplorerDetails } from "~/components/ExplorerDetails";
import { IconButton } from "~/components/IconButton";
import { Link } from "~/components/Link";
import { env } from "~/env.mjs";
import DiscordIcon from "~/icons/discord.svg";
Expand Down Expand Up @@ -34,7 +34,7 @@ export const BottomBarLayout = () => {
<div className="flex items-center gap-2">
{EXTERNAL_APPS.map(({ icon, href }) => (
<Link key={href} href={href} isExternal hideExternalIcon>
<Button variant="icon" icon={icon} size="md" />
<IconButton>{icon}</IconButton>
</Link>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/AppLayout/TopBarLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const TopBarLayout: React.FC = () => {
<div className="hidden xl:flex">
<NavigationMenus />
</div>
<div className="relative hidden xl:block">
<div className="relative ml-2 hidden xl:block">
<ThemeModeButton />
</div>
</div>
Expand Down
212 changes: 76 additions & 136 deletions apps/web/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,144 +1,84 @@
import type {
ButtonHTMLAttributes,
DOMAttributes,
FC,
HTMLAttributes,
ReactElement,
} from "react";
import classNames from "classnames";
import type { ButtonHTMLAttributes, FC } from "react";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { twMerge } from "tailwind-merge";

import type { Size } from "~/types";

type Variant = "outline" | "primary" | "icon";
type VariantStyles = Record<
Variant,
HTMLAttributes<HTMLButtonElement>["className"]
>;

type ButtonProps = {
disabled?: boolean;
type?: ButtonHTMLAttributes<HTMLButtonElement>["type"];
variant?: keyof VariantStyles;
className?: HTMLAttributes<HTMLButtonElement>["className"];
size?: Size;
icon?: ReactElement<{ className?: string }>;
label?: React.ReactNode;
onClick?: DOMAttributes<HTMLButtonElement>["onClick"];
};

const VARIANT_STYLES: VariantStyles = {
primary: `
bg-accent-light
dark:bg-primary-500
text-accentContent-light
dark:text-accentContent-dark
hover:bg-accentHighlight-light
dark:hover:bg-accentHighlight-dark
active:bg-accent-light
dark:active:bg-accent-dark
disabled:text-warmGray-400
dark:disabled:text-coolGray-400
disabled:bg-warmGray-500
dark:disabled:bg-coolGray-300
disabled:border-warmGray-500
dark:disabled:border-coolGray-300
const buttonVariants = cva(
`
rounded
text-sm
font-semibold
transition-colors
active:scale-[0.99]
disabled:pointer-events-none
disabled:cursor-default
`,
outline: `
bg-transparent
dark:bg-transparent
border-accent-light
dark:border-accent-dark
text-accent-light
dark:text-accent-dark
hover:text-content-dark
dark:hover:text-content-dark
hover:bg-accentHighlight-light
dark:hover:bg-accentHighlight-dark
active:bg-accent-light
dark:active:bg-accent-dark
border
{
variants: {
variant: {
primary: `
shadow-sm
bg-accent-light
dark:bg-primary-500
text-accentContent-light
dark:text-accentContent-dark
hover:bg-accentHighlight-light
dark:hover:bg-accentHighlight-dark
active:bg-accent-light
dark:active:bg-accent-dark
disabled:text-contentDisabled-light
disabled:border-accentDisabled-light
dark:disabled:text-contentDisabled-dark
dark:disabled:border-accentDisabled-dark
`,
icon: `
rounded-full p-2
text-icon-light
shadow-sm transition-colors
focus-visible:outline
focus-visible:outline-2
focus-visible:outline-offset-2
focus-visible:outline-iconHighlight-dark
dark:text-icon-dark
`,
};
disabled:text-warmGray-400
dark:disabled:text-coolGray-400
disabled:bg-warmGray-500
dark:disabled:bg-coolGray-300
disabled:border-warmGray-500
dark:disabled:border-coolGray-300
`,
outline: `
shadow-sm
bg-transparent
dark:bg-transparent
border-accent-light
dark:border-accent-dark
text-accent-light
dark:text-accent-dark
hover:text-content-dark
dark:hover:text-content-dark
hover:bg-accentHighlight-light
dark:hover:bg-accentHighlight-dark
active:bg-accent-light
dark:active:bg-accent-dark
border
disabled:text-contentDisabled-light
disabled:border-accentDisabled-light
dark:disabled:text-contentDisabled-dark
dark:disabled:border-accentDisabled-dark
`,
},
size: {
sm: "px-2 py-1 h-7",
md: "px-4 py-2 h-9",
lg: "px-4 py-3 h-11",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);

export const Button: FC<ButtonProps> = function ({
disabled = false,
type = "button",
className,
icon,
label,
onClick,
size = "md",
variant,
}: ButtonProps) {
type Props = ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants>;

const Button: FC<Props> = ({ className, variant, size, ...props }) => {
return (
<button
disabled={disabled}
type={type}
className={`
${VARIANT_STYLES[variant ?? "primary"]}
${
variant !== "icon"
? classNames({
"px-2 py-1": size === "sm",
"px-4 py-2": size === "md",
"px-4 py-3": size === "lg",
})
: `
fill-icon-light
text-icon-light
hover:fill-iconHighlight-light
hover:text-iconHighlight-light
dark:fill-icon-dark
dark:text-icon-dark
hover:dark:fill-iconHighlight-dark
hover:dark:text-iconHighlight-dark
${classNames({
"w-7": size === "sm",
"w-9": size === "md",
"w-11": size === "lg",
"w-13": size === "xl",
})}
`
}
${classNames({
"h-7": size === "sm",
"h-9": size === "md",
"h-11": size === "lg",
"h-13": size === "xl",
})}
cursor-pointer
rounded
text-sm
font-semibold
shadow-sm
transition-colors
active:scale-[0.99]
disabled:pointer-events-none
disabled:cursor-default
${className}
`}
onClick={onClick}
>
<div className="flex h-full items-center gap-1">
{icon && <div className="h-full w-full">{icon}</div>}
{label}
</div>
</button>
className={twMerge(buttonVariants({ variant, size }), className)}
{...props}
/>
);
};

export { Button };
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React, { useState } from "react";
import type { FC } from "react";
import { ArrowRightIcon, ChevronDownIcon } from "@heroicons/react/24/outline";

import { Button } from "~/components/Button";
import { Collapsable } from "~/components/Collapsable";
import { EtherUnitDisplay } from "~/components/Displays/EtherUnitDisplay";
import { IconButton } from "~/components/IconButton";
import { RollupIcon } from "~/components/RollupIcon";
import { Rotable } from "~/components/Rotable";
import { Skeleton } from "~/components/Skeleton";
Expand Down Expand Up @@ -196,11 +196,9 @@ const BlobTransactionCard: FC<BlobTransactionCardProps> = function ({
rotated={opened}
onClick={() => setOpened((prevOpened) => !prevOpened)}
>
<Button
variant="icon"
icon={<ChevronDownIcon className="h-5 w-5" />}
size="md"
/>
<IconButton>
<ChevronDownIcon />
</IconButton>
</Rotable>
</div>
)}
Expand Down
61 changes: 61 additions & 0 deletions apps/web/src/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ButtonHTMLAttributes, FC } from "react";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { twMerge } from "tailwind-merge";

const iconVariants = cva(
`
rounded
text-sm
font-semibold
transition-colors
active:scale-[0.99]
disabled:pointer-events-none
disabled:cursor-default
flex
items-center
justify-center
`,
{
variants: {
variant: {
default: `
focus-visible:outline
focus-visible:outline-2
focus-visible:outline-offset-2
focus-visible:outline-iconHighlight-dark
fill-icon-light
text-icon-light
hover:fill-iconHighlight-light
hover:text-iconHighlight-light
dark:fill-icon-dark
dark:text-icon-dark
hover:dark:fill-iconHighlight-dark
hover:dark:text-iconHighlight-dark
`,
},
size: {
md: "[&>*]:h-5 [&>*]:w-5",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
);

type Props = ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof iconVariants>;

const IconButton: FC<Props> = ({ className, variant, size, ...props }) => {
return (
<button
className={twMerge(iconVariants({ variant, size }), className)}
{...props}
/>
);
};

export { IconButton };
Loading

0 comments on commit be191ba

Please sign in to comment.