Refactor buttons

This commit is contained in:
Robert Long 2021-12-07 11:59:57 -08:00
commit 4cb144809f
19 changed files with 313 additions and 622 deletions

View file

@ -1,37 +0,0 @@
import React from "react";
import { useButton } from "@react-aria/button";
import useClipboard from "react-use-clipboard";
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
import { ReactComponent as CopyIcon } from "./icons/Copy.svg";
import classNames from "classnames";
import styles from "./CopyButton.module.css";
export function CopyButton({ value, className, children, ...rest }) {
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
const { buttonProps } = useButton({
onPress: () => setCopied(),
});
return (
<button
{...buttonProps}
className={classNames(
styles.copyButton,
{ [styles.copied]: isCopied },
className
)}
>
{isCopied ? (
<>
<span>Copied!</span>
<CheckIcon />
</>
) : (
<>
<span>{children || value}</span>
<CopyIcon />
</>
)}
</button>
);
}

View file

@ -1,43 +0,0 @@
.copyButton {
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
cursor: pointer;
border: 2px solid #0dbd8b;
border-radius: 8px;
color: #0dbd8b;
width: 100%;
height: 40px;
transition: border-color 250ms, background-color 250ms;
padding: 0 20px;
}
.copyButton span {
font-weight: 600;
font-size: 15px;
margin-right: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
flex: 1;
}
.copyButton svg {
flex-shrink: 0;
}
.copyButton:not(.copied) svg * {
fill: #0dbd8b;
}
.copyButton.copied {
border-color: transparent;
background-color: #0dbd8b;
color: white;
}
.copyButton.copied svg * {
stroke: white;
}

View file

@ -1,5 +1,5 @@
import React, { useCallback } from "react";
import { ButtonTooltip, HeaderButton } from "./RoomButton";
import React from "react";
import { ButtonTooltip, Button } from "./button";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
@ -11,10 +11,10 @@ import { Item } from "@react-stately/collections";
export function GridLayoutMenu({ layout, setLayout }) {
return (
<PopoverMenuTrigger placement="bottom right">
<HeaderButton>
<Button variant="icon">
<ButtonTooltip>Layout Type</ButtonTooltip>
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</HeaderButton>
</Button>
{(props) => (
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
<Item key="freedom" textValue="Freedom">

View file

@ -1,11 +1,11 @@
import classNames from "classnames";
import React from "react";
import React, { useRef } from "react";
import { Link } from "react-router-dom";
import styles from "./Header.module.css";
import { ReactComponent as LogoIcon } from "./Logo.svg";
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
import { HeaderDropdownItem, UserMenu } from "./RoomButton";
import { useButton } from "@react-aria/button";
export function Header({ children, className, ...rest }) {
return (
@ -56,25 +56,13 @@ export function RoomHeaderInfo({ roomName }) {
);
}
export function RoomSetupHeaderInfo({ onBack, roomName }) {
export function RoomSetupHeaderInfo({ roomName, ...rest }) {
const ref = useRef();
const { buttonProps } = useButton(rest, ref);
return (
<button className={styles.backButton} onClick={onBack}>
<button className={styles.backButton} ref={ref} {...buttonProps}>
<ArrowLeftIcon width={16} height={16} />
<RoomHeaderInfo roomName={roomName} />
</button>
);
}
export function UserDropdownMenu({ userName, signedIn, onLogout }) {
if (!signedIn) {
return null;
}
return (
<UserMenu userName={userName}>
<HeaderDropdownItem onClick={onLogout} className={styles.signOutButton}>
Sign Out
</HeaderDropdownItem>
</UserMenu>
);
}

View file

@ -23,7 +23,7 @@ import {
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import ColorHash from "color-hash";
import styles from "./Home.module.css";
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Center, Content, Modal } from "./Layout";
import {
GroupCallIntent,
@ -31,6 +31,7 @@ import {
} from "matrix-js-sdk/src/browser-index";
import { Facepile } from "./Facepile";
import { UserMenu } from "./UserMenu";
import { Button } from "./button";
const colorHash = new ColorHash({ lightness: 0.3 });

View file

@ -44,18 +44,6 @@ export const InputField = forwardRef(
}
);
export const Button = forwardRef(({ className, children, ...rest }, ref) => {
return (
<button
className={classNames(styles.button, className)}
ref={ref}
{...rest}
>
{children}
</button>
);
});
export function ErrorMessage({ children }) {
return <p className={styles.errorMessage}>{children}</p>;
}

View file

@ -151,29 +151,6 @@
border: 1.5px solid var(--inputBorderColorFocused) !important;
}
.button {
vertical-align: middle;
border: 0;
border-radius: 8px;
font-size: 14px;
color: #fff;
background-color: var(--primaryColor);
width: auto;
padding: 7px 15px;
cursor: pointer;
display: inline-block;
-webkit-box-sizing: border-box;
box-sizing: border-box;
text-align: center;
font-weight: 700;
}
.button:hover {
}
.button:active {
}
.errorMessage {
margin: 0;
font-size: 13px;

View file

@ -1,6 +1,6 @@
import React from "react";
import { Modal, ModalContent } from "./Modal";
import { CopyButton } from "./CopyButton";
import { CopyButton } from "./button";
export function InviteModal({ roomUrl, ...rest }) {
return (

View file

@ -17,8 +17,9 @@ limitations under the License.
import React, { useCallback, useRef, useState } from "react";
import { useHistory, useLocation, Link } from "react-router-dom";
import { Header, HeaderLogo, LeftNav } from "./Header";
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Center, Content, Info, Modal } from "./Layout";
import { Button } from "./button";
export function LoginPage({ onLogin }) {
const [homeserver, setHomeServer] = useState(

View file

@ -1,5 +1,5 @@
import React, { useCallback } from "react";
import { ButtonTooltip, RoomButton } from "./RoomButton";
import { ButtonTooltip, Button } from "./button";
import { Menu } from "./Menu";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { Item } from "@react-stately/collections";
@ -37,10 +37,10 @@ export function OverflowMenu({
return (
<>
<PopoverMenuTrigger>
<RoomButton>
<Button variant="toolbar">
<ButtonTooltip>More</ButtonTooltip>
<OverflowIcon />
</RoomButton>
</Button>
{(props) => (
<Menu {...props} label="More menu" onAction={onAction}>
<Item key="invite" textValue="Invite people">

View file

@ -17,8 +17,9 @@ limitations under the License.
import React, { useCallback, useRef, useState } from "react";
import { useHistory, useLocation, Link } from "react-router-dom";
import { Header, LeftNav, HeaderLogo } from "./Header";
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Center, Content, Info, Modal } from "./Layout";
import { Button } from "./button";
export function RegisterPage({ onRegister }) {
const registerUsernameRef = useRef();

View file

@ -18,11 +18,13 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import styles from "./Room.module.css";
import { useLocation, useParams, useHistory, Link } from "react-router-dom";
import {
Button,
CopyButton,
HangupButton,
MicButton,
VideoButton,
ScreenshareButton,
} from "./RoomButton";
} from "./button";
import {
Header,
LeftNav,
@ -30,7 +32,6 @@ import {
RoomHeaderInfo,
RoomSetupHeaderInfo,
} from "./Header";
import { Button } from "./Input";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import VideoGrid, {
useVideoGridLayout,
@ -47,7 +48,6 @@ import * as Sentry from "@sentry/react";
import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { UserMenu } from "./UserMenu";
import { CopyButton } from "./CopyButton";
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@ -276,7 +276,7 @@ function RoomSetupView({
<Header>
<LeftNav>
<RoomSetupHeaderInfo
onBack={() => history.goBack()}
onPress={() => history.goBack()}
roomName={roomName}
/>
</LeftNav>
@ -310,7 +310,7 @@ function RoomSetupView({
<Button
className={styles.joinCallButton}
disabled={state !== GroupCallState.LocalCallFeedInitialized}
onClick={onEnter}
onPress={onEnter}
>
Join call now
</Button>

View file

@ -1,255 +0,0 @@
import React, { useRef, useState, useEffect, forwardRef } from "react";
import classNames from "classnames";
import styles from "./RoomButton.module.css";
import { ReactComponent as MicIcon } from "./icons/Mic.svg";
import { ReactComponent as MuteMicIcon } from "./icons/MuteMic.svg";
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
import { ReactComponent as DisableVideoIcon } from "./icons/DisableVideo.svg";
import { ReactComponent as HangupIcon } from "./icons/Hangup.svg";
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
import { ReactComponent as ScreenshareIcon } from "./icons/Screenshare.svg";
import { ReactComponent as ChevronIcon } from "./icons/Chevron.svg";
import { ReactComponent as UserIcon } from "./icons/User.svg";
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
import { useButton } from "@react-aria/button";
export const RoomButton = forwardRef(
({ on, off, className, children, ...rest }, ref) => {
const { buttonProps } = useButton(rest, ref);
return (
<button
className={classNames(styles.roomButton, className, {
[styles.on]: on,
[styles.off]: off,
})}
{...buttonProps}
ref={ref}
>
{children}
</button>
);
}
);
export function DropdownButton({ onChange, options, value, children }) {
const buttonRef = useRef();
const [open, setOpen] = useState(false);
useEffect(() => {
function onClick() {
if (open) {
setOpen(false);
}
}
window.addEventListener("click", onClick);
return () => {
window.removeEventListener("click", onClick);
};
}, [open]);
return (
<div className={styles.dropdownButtonContainer}>
{children}
<button
ref={buttonRef}
className={styles.dropdownButton}
onClick={() => setOpen(true)}
>
<ChevronIcon />
</button>
{open && (
<div className={styles.dropdownContainer}>
<ul>
{options.map((item) => (
<li
key={item.value}
className={classNames({
[styles.dropdownActiveItem]: item.value === value,
})}
onClick={() => onChange(item)}
>
{item.label}
</li>
))}
</ul>
</div>
)}
</div>
);
}
export function MicButton({ muted, ...rest }) {
return (
<RoomButton {...rest} off={muted}>
<ButtonTooltip>
{muted ? "Unmute microphone" : "Mute microphone"}
</ButtonTooltip>
{muted ? <MuteMicIcon /> : <MicIcon />}
</RoomButton>
);
}
export function VideoButton({ muted, ...rest }) {
return (
<RoomButton {...rest} off={muted}>
<ButtonTooltip>
{muted ? "Turn on camera" : "Turn off camera"}
</ButtonTooltip>
{muted ? <DisableVideoIcon /> : <VideoIcon />}
</RoomButton>
);
}
export function ScreenshareButton({ enabled, className, ...rest }) {
return (
<RoomButton {...rest} on={enabled}>
<ButtonTooltip>
{enabled ? "Stop sharing screen" : "Share screen"}
</ButtonTooltip>
<ScreenshareIcon />
</RoomButton>
);
}
export function HangupButton({ className, ...rest }) {
return (
<RoomButton
className={classNames(styles.hangupButton, className)}
{...rest}
>
<ButtonTooltip>Leave</ButtonTooltip>
<HangupIcon />
</RoomButton>
);
}
export const HeaderButton = forwardRef(
({ on, className, children, ...rest }, ref) => {
const { buttonProps } = useButton(rest, ref);
return (
<button
className={classNames(styles.headerButton, className, {
[styles.on]: on,
})}
{...buttonProps}
ref={ref}
>
{children}
</button>
);
}
);
export function HeaderDropdownButton({ children, content }) {
const buttonRef = useRef();
const [open, setOpen] = useState(false);
useEffect(() => {
function onClick() {
if (open) {
setOpen(false);
}
}
window.addEventListener("click", onClick);
return () => {
window.removeEventListener("click", onClick);
};
}, [open]);
return (
<div className={styles.dropdownButtonContainer}>
<button
ref={buttonRef}
className={classNames(styles.headerButton, { [styles.on]: open })}
onClick={() => setOpen(true)}
>
{children}
</button>
{open && (
<div
className={classNames(
styles.dropdownContainer,
styles.headerDropdownContainer
)}
>
<ul>{content}</ul>
</div>
)}
</div>
);
}
export function HeaderDropdownItem({ active, children, className, ...rest }) {
return (
<li
className={classNames(className, {
[styles.dropdownActiveItem]: active,
})}
{...rest}
>
{children}
</li>
);
}
export function UserMenu({ userName, children }) {
return (
<HeaderDropdownButton content={children}>
<ButtonTooltip>Profile</ButtonTooltip>
<div className={styles.userButton}>
<UserIcon />
<span>{userName}</span>
</div>
</HeaderDropdownButton>
);
}
export function SettingsButton(props) {
return (
<HeaderButton {...props}>
<ButtonTooltip>Show Dev Tools</ButtonTooltip>
<SettingsIcon width={20} height={20} />
</HeaderButton>
);
}
export function LayoutToggleButton({ layout, setLayout, ...rest }) {
return (
<HeaderDropdownButton
{...rest}
content={
<>
<HeaderDropdownItem onClick={() => setLayout("freedom")}>
<FreedomIcon />
<span>Freedom</span>
{layout === "freedom" && <CheckIcon className={styles.checkIcon} />}
</HeaderDropdownItem>
<HeaderDropdownItem onClick={() => setLayout("spotlight")}>
<SpotlightIcon />
<span>Spotlight</span>
{layout === "spotlight" && (
<CheckIcon className={styles.checkIcon} />
)}
</HeaderDropdownItem>
</>
}
>
<ButtonTooltip>Layout Type</ButtonTooltip>
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</HeaderDropdownButton>
);
}
export function ButtonTooltip({ className, children }) {
return (
<div className={classNames(styles.buttonTooltip, className)}>
{children}
</div>
);
}

View file

@ -1,213 +0,0 @@
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.roomButton,
.headerButton,
.dropdownButton {
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
padding: 0;
border: none;
cursor: pointer;
}
.roomButton {
width: 50px;
height: 50px;
border-radius: 50px;
background-color: #394049;
}
.roomButton:hover {
background-color: #8d97a5;
}
.roomButton:active {
}
.roomButton.on,
.roomButton.off {
background-color: #ffffff;
}
.headerButton svg * {
fill: #8e99a4;
}
.headerButton:hover svg * {
fill: #8d97a5;
}
.headerButton:hover svg * {
fill: #8d97a5;
}
.headerButton.on svg * {
fill: #0dbd8b;
}
.hangupButton,
.hangupButton:hover {
background-color: #ff5b55;
}
.roomButton.on svg * {
fill: #0dbd8b;
}
.roomButton.off svg * {
fill: #21262c;
}
.dropdownButtonContainer {
position: relative;
}
.dropdownButton {
width: 15px;
height: 15px;
border-radius: 15px;
background-color: #394049;
position: absolute;
bottom: 0;
right: 0;
cursor: pointer;
}
.dropdownButton:hover {
background-color: #8d97a5;
}
.dropdownButton:hover svg * {
fill: #8d97a5;
}
.dropdownContainer {
position: absolute;
left: 50%;
transform: translate(0, -100%);
top: -5px;
background-color: var(--bgColor4);
border-radius: 8px;
overflow: hidden;
}
.headerDropdownContainer {
transform: translate(-100%, 36px);
left: 100%;
z-index: 1;
}
.checkIcon {
position: absolute;
right: 16px;
}
.checkIcon * {
stroke: var(--textColor1);
}
.dropdownContainer ul {
list-style: none;
margin: 0;
padding: 0;
}
.dropdownContainer li {
position: relative;
padding: 12px;
width: 200px;
cursor: pointer;
font-size: 15px;
display: flex;
align-items: center;
}
.dropdownContainer li > * {
margin-right: 5px;
}
.dropdownContainer li > :last-child {
margin-right: 0;
}
.dropdownContainer li:hover {
background-color: var(--bgColor5);
}
.dropdownActiveItem {
color: #0dbd8b;
}
.buttonTooltip {
display: none;
background-color: var(--bgColor2);
position: absolute;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 8px 10px;
color: var(--textColor1);
border-radius: 8px;
max-width: 135px;
width: max-content;
z-index: 1;
}
.buttonTooltip.bottomRight {
right: 0;
}
.roomButton:hover .buttonTooltip {
display: flex;
bottom: calc(100% + 6px);
}
.headerButton:hover .buttonTooltip {
display: flex;
top: calc(100% + 6px);
}
.userButton {
display: flex;
align-items: center;
color: var(--textColor1);
font-weight: 600;
font-size: 15px;
}
.userButton > * {
margin-right: 5px;
}
.userButton svg * {
fill: var(--textColor1) !important;
}
.headerButton:hover .userButton > * {
color: var(--textColor1);
}
.headerButton:hover .userButton svg * {
fill: var(--textColor1);
}
.userButton > :last-child {
margin-right: 0;
}

View file

@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from "react";
import { ButtonTooltip, HeaderButton } from "./RoomButton";
import { ButtonTooltip, Button } from "./button";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { ReactComponent as UserIcon } from "./icons/User.svg";
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
@ -48,13 +48,13 @@ export function UserMenu({ userName, signedIn, onLogin, onLogout }) {
}, [signedIn, userName]);
return (
<PopoverMenuTrigger onAction={onAction} placement="bottom right">
<HeaderButton className={styles.userButton}>
<PopoverMenuTrigger placement="bottom right">
<Button variant="icon" className={styles.userButton}>
<ButtonTooltip>Profile</ButtonTooltip>
<UserIcon />
</HeaderButton>
</Button>
{(props) => (
<Menu {...props} label="User menu">
<Menu {...props} label="User menu" onAction={onAction}>
{items.map(({ key, icon: Icon, label }) => (
<Item key={key} textValue={label}>
<Icon />

101
src/button/Button.jsx Normal file
View file

@ -0,0 +1,101 @@
import React, { forwardRef } from "react";
import classNames from "classnames";
import styles from "./Button.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
import { useButton } from "@react-aria/button";
import { useObjectRef } from "@react-aria/utils";
const variantToClassName = {
default: [styles.button],
toolbar: [styles.toolbarButton],
icon: [styles.iconButton],
copy: [styles.copyButton],
};
export const Button = forwardRef(
({ variant = "default", on, off, className, children, ...rest }, ref) => {
const buttonRef = useObjectRef(ref);
const { buttonProps } = useButton(rest, buttonRef);
// TODO: react-aria's useButton hook prevents form submission via keyboard
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
let filteredButtonProps = buttonProps;
if (rest.type === "submit" && !rest.onPress) {
const { onKeyDown, onKeyUp, ...filtered } = buttonProps;
filteredButtonProps = filtered;
}
return (
<button
className={classNames(variantToClassName[variant], className, {
[styles.on]: on,
[styles.off]: off,
})}
{...filteredButtonProps}
ref={buttonRef}
>
{children}
</button>
);
}
);
export function ButtonTooltip({ className, children }) {
return (
<div className={classNames(styles.buttonTooltip, className)}>
{children}
</div>
);
}
export function MicButton({ muted, ...rest }) {
return (
<Button variant="toolbar" {...rest} off={muted}>
<ButtonTooltip>
{muted ? "Unmute microphone" : "Mute microphone"}
</ButtonTooltip>
{muted ? <MuteMicIcon /> : <MicIcon />}
</Button>
);
}
export function VideoButton({ muted, ...rest }) {
return (
<Button variant="toolbar" {...rest} off={muted}>
<ButtonTooltip>
{muted ? "Turn on camera" : "Turn off camera"}
</ButtonTooltip>
{muted ? <DisableVideoIcon /> : <VideoIcon />}
</Button>
);
}
export function ScreenshareButton({ enabled, className, ...rest }) {
return (
<Button variant="toolbar" {...rest} on={enabled}>
<ButtonTooltip>
{enabled ? "Stop sharing screen" : "Share screen"}
</ButtonTooltip>
<ScreenshareIcon />
</Button>
);
}
export function HangupButton({ className, ...rest }) {
return (
<Button
variant="toolbar"
className={classNames(styles.hangupButton, className)}
{...rest}
>
<ButtonTooltip>Leave</ButtonTooltip>
<HangupIcon />
</Button>
);
}

View file

@ -0,0 +1,155 @@
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.button,
.toolbarButton,
.iconButton {
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
padding: 0;
border: none;
cursor: pointer;
}
.button {
color: #fff;
background-color: var(--primaryColor);
padding: 7px 15px;
border-radius: 8px;
font-size: 14px;
font-weight: 700;
}
.toolbarButton {
width: 50px;
height: 50px;
border-radius: 50px;
background-color: #394049;
}
.toolbarButton:hover {
background-color: #8d97a5;
}
.toolbarButton.on,
.toolbarButton.off {
background-color: #ffffff;
}
.iconButton svg * {
fill: #8e99a4;
}
.iconButton:hover svg * {
fill: #8d97a5;
}
.iconButton:hover svg * {
fill: #8d97a5;
}
.iconButton.on svg * {
fill: #0dbd8b;
}
.hangupButton,
.hangupButton:hover {
background-color: #ff5b55;
}
.toolbarButton.on svg * {
fill: #0dbd8b;
}
.toolbarButton.off svg * {
fill: #21262c;
}
.buttonTooltip {
display: none;
background-color: var(--bgColor2);
position: absolute;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 8px 10px;
color: var(--textColor1);
border-radius: 8px;
max-width: 135px;
width: max-content;
z-index: 1;
}
.buttonTooltip.bottomRight {
right: 0;
}
.toolbarButton:hover .buttonTooltip {
display: flex;
bottom: calc(100% + 6px);
}
.iconButton:hover .buttonTooltip {
display: flex;
top: calc(100% + 6px);
}
.copyButton {
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
cursor: pointer;
border: 2px solid #0dbd8b;
border-radius: 8px;
color: #0dbd8b;
width: 100%;
height: 40px;
transition: border-color 250ms, background-color 250ms;
padding: 0 20px;
}
.copyButton span {
font-weight: 600;
font-size: 15px;
margin-right: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
flex: 1;
}
.copyButton svg {
flex-shrink: 0;
}
.copyButton:not(.on) svg * {
fill: #0dbd8b;
}
.copyButton.on {
border-color: transparent;
background-color: #0dbd8b;
color: white;
}
.copyButton.on svg * {
stroke: white;
}

25
src/button/CopyButton.jsx Normal file
View file

@ -0,0 +1,25 @@
import React from "react";
import useClipboard from "react-use-clipboard";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
import { Button } from "./Button";
export function CopyButton({ value, children, ...rest }) {
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
return (
<Button {...rest} variant="copy" on={isCopied} onPress={setCopied}>
{isCopied ? (
<>
<span>Copied!</span>
<CheckIcon />
</>
) : (
<>
<span>{children || value}</span>
<CopyIcon />
</>
)}
</Button>
);
}

2
src/button/index.js Normal file
View file

@ -0,0 +1,2 @@
export * from "./Button";
export * from "./CopyButton";