Refactor buttons (#1257)

* Refactor buttons

* Complete components creation

* Complete refactoring

* fix lint

* Complete button work
This commit is contained in:
Charles Bochet
2023-08-26 23:59:45 +02:00
committed by GitHub
parent 5d50bbd6a3
commit 1b187350c0
57 changed files with 2209 additions and 859 deletions

View File

@ -1,6 +1,6 @@
import { useTheme } from '@emotion/react';
import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon/index';
@ -17,7 +17,7 @@ export function ActivityCreateButton({
}: ActivityCreateButtonProps) {
const theme = useTheme();
return (
<ButtonGroup variant={ButtonVariant.Secondary}>
<ButtonGroup variant={'secondary'}>
<Button
icon={<IconNotes size={theme.icon.size.sm} />}
title="Note"

View File

@ -6,11 +6,7 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct
import { NoteList } from '@/activities/notes/components/NoteList';
import { useNotes } from '@/activities/notes/hooks/useNotes';
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
import {
Button,
ButtonSize,
ButtonVariant,
} from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { ActivityType } from '~/generated/graphql';
const StyledTaskGroupEmptyContainer = styled.div`
@ -64,7 +60,7 @@ export function Notes({ entity }: { entity: ActivityTargetableEntity }) {
<Button
icon={<IconNotes size={theme.icon.size.sm} />}
title="New note"
variant={ButtonVariant.Secondary}
variant="secondary"
onClick={() => openCreateActivity(ActivityType.Note, [entity])}
/>
</StyledTaskGroupEmptyContainer>
@ -79,8 +75,8 @@ export function Notes({ entity }: { entity: ActivityTargetableEntity }) {
button={
<Button
icon={<IconNotes size={theme.icon.size.md} />}
size={ButtonSize.Small}
variant={ButtonVariant.Secondary}
size="small"
variant="secondary"
title="Add note"
onClick={() => openCreateActivity(ActivityType.Note, [entity])}
></Button>

View File

@ -1,28 +1,20 @@
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities';
import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/graphql/queries/getActivitiesByTarget';
import { GET_COMPANIES } from '@/companies/graphql/queries/getCompanies';
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
import { IconButton } from '@/ui/button/components/IconButton';
import { LightIconButton } from '@/ui/button/components/LightIconButton';
import { IconTrash } from '@/ui/icon';
import { isRightDrawerOpenState } from '@/ui/right-drawer/states/isRightDrawerOpenState';
import { useDeleteActivityMutation } from '~/generated/graphql';
const StyledContainer = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
`;
type OwnProps = {
activityId: string;
};
export function ActivityActionBar({ activityId }: OwnProps) {
const theme = useTheme();
const [deleteActivityMutation] = useDeleteActivityMutation();
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
@ -40,13 +32,11 @@ export function ActivityActionBar({ activityId }: OwnProps) {
}
return (
<StyledContainer>
<IconButton
icon={
<IconTrash size={theme.icon.size.sm} stroke={theme.icon.stroke.md} />
}
onClick={deleteActivity}
/>
</StyledContainer>
<LightIconButton
icon={<IconTrash />}
onClick={deleteActivity}
accent="tertiary"
size="medium"
/>
);
}

View File

@ -2,11 +2,7 @@ import { useTheme } from '@emotion/react';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
import {
Button,
ButtonSize,
ButtonVariant,
} from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { IconPlus } from '@/ui/icon';
import { ActivityType } from '~/generated/graphql';
@ -25,8 +21,8 @@ export function AddTaskButton({
return (
<Button
icon={<IconPlus size={theme.icon.size.md} />}
size={ButtonSize.Small}
variant={ButtonVariant.Secondary}
size="small"
variant="secondary"
title="Add task"
onClick={() => openCreateActivity(ActivityType.Task, [entity])}
></Button>

View File

@ -5,7 +5,7 @@ import { IconCheckbox } from '@tabler/icons-react';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { useTasks } from '@/activities/tasks/hooks/useTasks';
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { ActivityType } from '~/generated/graphql';
import { AddTaskButton } from './AddTaskButton';
@ -69,7 +69,7 @@ export function TaskGroups({ entity, showAddButton }: OwnProps) {
<Button
icon={<IconCheckbox size={theme.icon.size.sm} />}
title="New task"
variant={ButtonVariant.Secondary}
variant={'secondary'}
onClick={() =>
openCreateActivity(ActivityType.Task, entity ? [entity] : undefined)
}

View File

@ -10,8 +10,7 @@ import {
PersonForSelect,
} from '@/people/components/PeoplePicker';
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
import { ButtonSize } from '@/ui/button/components/Button';
import { IconButton } from '@/ui/button/components/IconButton';
import { LightIconButton } from '@/ui/button/components/LightIconButton';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { TextInput } from '@/ui/input/text/components/TextInput';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
@ -123,11 +122,11 @@ export function AddPersonToCompany({
<RecoilScope>
<StyledContainer>
<div ref={refs.setReference}>
<IconButton
icon={<IconPlus size={14} />}
<LightIconButton
icon={<IconPlus />}
onClick={handleOpenPicker}
size={ButtonSize.Small}
variant={'transparent'}
size="small"
accent="tertiary"
/>
</div>

View File

@ -16,11 +16,11 @@ export function CompanyTableMockData() {
);
const setEntityTableData = useSetEntityTableData();
setEntityTableData(mockedCompaniesData, []);
useEffect(() => {
setEntityTableData(mockedCompaniesData, []);
setColumns(companyViewFields);
}, [setColumns]);
}, [setColumns, setEntityTableData]);
return <></>;
}

View File

@ -6,7 +6,7 @@ import styled from '@emotion/styled';
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
import { IconDotsVertical, IconLinkOff, IconTrash } from '@tabler/icons-react';
import { IconButton } from '@/ui/button/components/IconButton';
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
@ -165,11 +165,10 @@ export function PeopleCard({
</StyledCardInfo>
{isHovered && (
<div ref={refs.setReference}>
<IconButton
<FloatingIconButton
onClick={handleToggleOptions}
variant="shadow"
size="small"
icon={<IconDotsVertical size={theme.icon.size.md} />}
icon={<IconDotsVertical />}
/>
{isOptionsOpen && (
<StyledDropdownMenu ref={refs.setFloating} style={floatingStyles}>
@ -177,15 +176,11 @@ export function PeopleCard({
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuSelectableItem onClick={handleDetachPerson}>
<IconButton icon={<IconLinkOff size={14} />} size="small" />
<IconLinkOff size={14} />
Detach relation
</DropdownMenuSelectableItem>
<DropdownMenuSelectableItem onClick={handleDeletePerson}>
<IconButton
icon={<IconTrash size={14} />}
size="small"
textColor="danger"
/>
<IconTrash size={14} color={theme.font.color.danger} />
<StyledRemoveOption>Delete person</StyledRemoveOption>
</DropdownMenuSelectableItem>
</StyledDropdownMenuItemsContainer>

View File

@ -57,10 +57,10 @@ export function PipelineAddButton() {
buttonComponents={
<IconButton
icon={<IconPlus size={16} />}
size="large"
data-testid="add-company-progress-button"
textColor={'secondary'}
variant="border"
size="medium"
dataTestId="add-company-progress-button"
accent="default"
variant="secondary"
onClick={toggleDropdownButton}
/>
}

View File

@ -5,11 +5,8 @@ import { useRecoilValue } from 'recoil';
import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { AppPath } from '@/types/AppPath';
import { ButtonVariant } from '@/ui/button/components/Button';
import {
ConfirmationModal,
StyledConfirmationButton,
} from '@/ui/modal/components/ConfirmationModal';
import { Button } from '@/ui/button/components/Button';
import { ConfirmationModal } from '@/ui/modal/components/ConfirmationModal';
import { H2Title } from '@/ui/typography/components/H2Title';
import { useDeleteUserAccountMutation } from '~/generated/graphql';
@ -40,9 +37,11 @@ export function DeleteAccount() {
description="Delete account and all the associated data"
/>
<StyledConfirmationButton
<Button
accent="danger"
size="small"
onClick={() => setIsDeleteAccountModalOpen(true)}
variant={ButtonVariant.Secondary}
variant="secondary"
title="Delete account"
/>

View File

@ -5,7 +5,6 @@ import { useRecoilValue } from 'recoil';
import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { AppPath } from '@/types/AppPath';
import { ButtonVariant } from '@/ui/button/components/Button';
import {
ConfirmationModal,
StyledConfirmationButton,
@ -38,7 +37,7 @@ export function DeleteWorkspace() {
<H2Title title="Danger zone" description="Delete your whole workspace" />
<StyledConfirmationButton
onClick={() => setIsDeleteWorkSpaceModalOpen(true)}
variant={ButtonVariant.Secondary}
variant="secondary"
title="Delete workspace"
/>

View File

@ -1,9 +1,7 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { ButtonVariant } from '@/ui/button/components/Button';
import { IconButton } from '@/ui/button/components/IconButton';
import { useDialog } from '@/ui/dialog/hooks/useDialog';
import { IconX } from '@/ui/icon/index';
@ -25,8 +23,6 @@ type ModalCloseButtonProps = {
};
export const ModalCloseButton = ({ onClose }: ModalCloseButtonProps) => {
const theme = useTheme();
const { initialStepState } = useSpreadsheetImportInternal();
const { initialStep } = useSpreadsheetImportInitialStep(
@ -49,19 +45,14 @@ export const ModalCloseButton = ({ onClose }: ModalCloseButtonProps) => {
message: 'Are you sure? Your current information will not be saved.',
buttons: [
{ title: 'Cancel' },
{ title: 'Exit', onClick: onClose, variant: ButtonVariant.Danger },
{ title: 'Exit', onClick: onClose, accent: 'danger' },
],
});
}
return (
<>
<StyledCloseButtonContainer>
<IconButton
icon={<IconX size={16} color={theme.font.color.tertiary} />}
onClick={handleClose}
/>
</StyledCloseButtonContainer>
</>
<StyledCloseButtonContainer>
<IconButton icon={<IconX />} onClick={handleClose} />
</StyledCloseButtonContainer>
);
};

View File

@ -11,7 +11,6 @@ import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableDat
import { setColumn } from '@/spreadsheet-import/utils/setColumn';
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
import { ButtonVariant } from '@/ui/button/components/Button';
import { useDialog } from '@/ui/dialog/hooks/useDialog';
import { Modal } from '@/ui/modal/components/Modal';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
@ -224,7 +223,7 @@ export const MatchColumnsStep = <T extends string>({
{
title: 'Continue',
onClick: handleAlertOnContinue,
variant: ButtonVariant.Primary,
variant: 'primary',
},
],
});

View File

@ -8,7 +8,7 @@ import { Table } from '@/spreadsheet-import/components/Table';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import type { Data } from '@/spreadsheet-import/types';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { useDialog } from '@/ui/dialog/hooks/useDialog';
import { IconTrash } from '@/ui/icon';
import { Toggle } from '@/ui/input/toggle/components/Toggle';
@ -173,7 +173,7 @@ export const ValidationStep = <T extends string>({
{ title: 'Cancel' },
{
title: 'Submit',
variant: ButtonVariant.Primary,
variant: 'primary',
onClick: submitData,
},
],
@ -201,7 +201,7 @@ export const ValidationStep = <T extends string>({
<Button
icon={<IconTrash />}
title="Remove"
variant={ButtonVariant.Danger}
accent="danger"
onClick={deleteSelectedRows}
disabled={selectedRows.size === 0}
/>

View File

@ -3,79 +3,245 @@ import styled from '@emotion/styled';
import { TablerIconsProps } from '@tabler/icons-react';
import { SoonPill } from '@/ui/pill/components/SoonPill';
import { rgba } from '@/ui/theme/constants/colors';
export enum ButtonSize {
Medium = 'medium',
Small = 'small',
}
export enum ButtonPosition {
Left = 'left',
Middle = 'middle',
Right = 'right',
}
export enum ButtonVariant {
Primary = 'primary',
Secondary = 'secondary',
Tertiary = 'tertiary',
TertiaryBold = 'tertiaryBold',
TertiaryLight = 'tertiaryLight',
Danger = 'danger',
}
export type ButtonSize = 'medium' | 'small';
export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
export type ButtonAccent = 'default' | 'blue' | 'danger';
export type ButtonProps = {
className?: string;
icon?: React.ReactNode;
title?: string;
fullWidth?: boolean;
variant?: ButtonVariant;
size?: ButtonSize;
position?: ButtonPosition;
accent?: ButtonAccent;
soon?: boolean;
disabled?: boolean;
} & React.ComponentProps<'button'>;
focus?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
const StyledButton = styled.button<
Pick<ButtonProps, 'fullWidth' | 'variant' | 'size' | 'position' | 'title'>
Pick<
ButtonProps,
'fullWidth' | 'variant' | 'size' | 'position' | 'accent' | 'focus'
>
>`
align-items: center;
background: ${({ theme, variant, disabled }) => {
${({ theme, variant, accent, disabled, focus }) => {
switch (variant) {
case 'primary':
if (disabled) {
return rgba(theme.color.blue, 0.4);
} else {
return theme.color.blue;
switch (accent) {
case 'default':
return `
background: ${theme.background.secondary};
border-color: ${
!disabled
? focus
? theme.color.blue
: theme.background.transparent.light
: 'transparent'
};
color: ${
!disabled
? theme.font.color.secondary
: theme.font.color.extraLight
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'
};
&:hover {
background: ${
!disabled
? theme.background.tertiary
: theme.background.secondary
};
}
&:active {
background: ${
!disabled
? theme.background.quaternary
: theme.background.secondary
};
}
`;
case 'blue':
return `
background: ${!disabled ? theme.color.blue : theme.color.blue20};
border-color: ${
!disabled
? focus
? theme.color.blue
: theme.background.transparent.light
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
color: ${theme.grayScale.gray0};
box-shadow: ${
!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'
};
&:hover {
background: ${
!disabled ? theme.color.blue50 : theme.color.blue20
};
}
&:active {
background: ${
!disabled ? theme.color.blue60 : theme.color.blue20
};
}
`;
case 'danger':
return `
background: ${!disabled ? theme.color.red : theme.color.red20};
border-color: ${
!disabled
? focus
? theme.color.red
: theme.background.transparent.light
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus ? `0 0 0 3px ${theme.color.red10}` : 'none'
};
color: ${theme.grayScale.gray0};
&:hover {
background: ${
!disabled ? theme.color.red50 : theme.color.red20
};
}
&:active {
background: ${
!disabled ? theme.color.red50 : theme.color.red20
};
}
`;
}
break;
case 'secondary':
return theme.background.primary;
default:
return 'transparent';
}
}};
border-color: ${({ theme, variant }) => {
switch (variant) {
case 'primary':
case 'secondary':
return `${theme.background.transparent.medium}`;
case 'danger':
return `${theme.border.color.danger}`;
case 'tertiary':
default:
return 'none';
switch (accent) {
case 'default':
return `
background: ${
focus ? theme.background.transparent.primary : 'transparent'
};
border-color: ${
variant === 'secondary'
? !disabled && focus
? theme.color.blue
: theme.background.transparent.light
: focus
? theme.color.blue
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'
};
color: ${
!disabled
? theme.font.color.secondary
: theme.font.color.extraLight
};
&:hover {
background: ${
!disabled ? theme.background.transparent.light : 'transparent'
};
}
&:active {
background: ${
!disabled ? theme.background.transparent.light : 'transparent'
};
}
`;
case 'blue':
return `
background: ${
focus ? theme.background.transparent.primary : 'transparent'
};
border-color: ${
variant === 'secondary'
? focus
? theme.color.blue
: theme.color.blue20
: focus
? theme.color.blue
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'
};
color: ${!disabled ? theme.color.blue : theme.accent.accent4060};
&:hover {
background: ${
!disabled ? theme.accent.tertiary : 'transparent'
};
}
&:active {
background: ${
!disabled ? theme.accent.secondary : 'transparent'
};
}
`;
case 'danger':
return `
background: ${
!disabled ? theme.background.transparent.primary : 'transparent'
};
border-color: ${
variant === 'secondary'
? focus
? theme.color.red
: theme.color.red20
: focus
? theme.color.red
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus ? `0 0 0 3px ${theme.color.red10}` : 'none'
};
color: ${!disabled ? theme.font.color.danger : theme.color.red20};
&:hover {
background: ${
!disabled ? theme.background.danger : 'transparent'
};
}
&:active {
background: ${
!disabled ? theme.background.danger : 'transparent'
};
}
`;
}
}
}};
border-radius: ${({ position }) => {
}}
border-radius: ${({ position, theme }) => {
switch (position) {
case 'left':
return '4px 0px 0px 4px';
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
case 'right':
return '0px 4px 4px 0px';
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
case 'middle':
return '0px';
default:
return '4px';
case 'standalone':
return theme.border.radius.sm;
}
}};
border-style: solid;
@ -83,68 +249,20 @@ const StyledButton = styled.button<
switch (variant) {
case 'primary':
case 'secondary':
case 'danger':
return position === 'middle' ? `1px 0 1px 0` : `1px`;
return position === 'middle' ? '1px 0px' : '1px';
case 'tertiary':
default:
return '0';
}
}};
box-shadow: ${({ theme, variant }) => {
switch (variant) {
case 'primary':
case 'secondary':
return theme.boxShadow.extraLight;
default:
return 'none';
}
}};
color: ${({ theme, variant, disabled }) => {
if (disabled) {
switch (variant) {
case 'primary':
return theme.grayScale.gray0;
case 'danger':
return theme.border.color.danger;
default:
return theme.font.color.extraLight;
}
}
switch (variant) {
case 'primary':
return theme.grayScale.gray0;
case 'tertiaryLight':
return theme.font.color.tertiary;
case 'danger':
return theme.color.red;
default:
return theme.font.color.secondary;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme, variant }) => {
switch (variant) {
case 'tertiary':
case 'tertiaryLight':
return theme.font.weight.regular;
default:
return theme.font.weight.medium;
}
}};
font-weight: 500;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: flex-start;
padding: ${({ theme, title }) => {
if (!title) {
return `${theme.spacing(1)}`;
}
return `${theme.spacing(2)} ${theme.spacing(3)}`;
padding: ${({ theme }) => {
return `0 ${theme.spacing(2)}`;
}};
transition: background 0.1s ease;
@ -153,49 +271,24 @@ const StyledButton = styled.button<
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
&:hover,
&:active {
${({ theme, variant, disabled }) => {
if (disabled) {
return '';
}
switch (variant) {
case 'primary':
return `background: linear-gradient(0deg, ${theme.background.transparent.medium} 0%, ${theme.background.transparent.medium} 100%), ${theme.color.blue}`;
case 'danger':
return `background: ${theme.background.transparent.danger}`;
default:
return `background: ${theme.background.tertiary}`;
}
}};
}
&:focus {
outline: none;
${({ theme, variant }) => {
switch (variant) {
case 'tertiaryLight':
case 'tertiaryBold':
case 'tertiary':
return `color: ${theme.color.blue};`;
default:
return '';
}
}};
}
`;
export function Button({
className,
icon: initialIcon,
title,
fullWidth = false,
variant = ButtonVariant.Primary,
size = ButtonSize.Medium,
position,
variant = 'primary',
size = 'medium',
accent = 'default',
position = 'standalone',
soon = false,
disabled = false,
...props
focus = false,
onClick,
}: ButtonProps) {
const icon = useMemo(() => {
if (!initialIcon || !React.isValidElement(initialIcon)) {
@ -214,8 +307,10 @@ export function Button({
size={size}
position={position}
disabled={soon || disabled}
title={title}
{...props}
focus={focus}
accent={accent}
className={className}
onClick={onClick}
>
{icon}
{title}

View File

@ -8,11 +8,19 @@ const StyledButtonGroupContainer = styled.div`
display: flex;
`;
type ButtonGroupProps = Pick<ButtonProps, 'variant' | 'size'> & {
export type ButtonGroupProps = Pick<
ButtonProps,
'variant' | 'size' | 'accent'
> & {
children: ReactNode[];
};
export function ButtonGroup({ children, variant, size }: ButtonGroupProps) {
export function ButtonGroup({
children,
variant,
size,
accent,
}: ButtonGroupProps) {
return (
<StyledButtonGroupContainer>
{React.Children.map(children, (child, index) => {
@ -21,19 +29,23 @@ export function ButtonGroup({ children, variant, size }: ButtonGroupProps) {
let position: ButtonPosition;
if (index === 0) {
position = ButtonPosition.Left;
position = 'left';
} else if (index === children.length - 1) {
position = ButtonPosition.Right;
position = 'right';
} else {
position = ButtonPosition.Middle;
position = 'middle';
}
const additionalProps: any = { position };
const additionalProps: any = { position, variant, accent, size };
if (variant) {
additionalProps.variant = variant;
}
if (accent) {
additionalProps.variant = variant;
}
if (size) {
additionalProps.size = size;
}

View File

@ -1,144 +0,0 @@
import React, { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { IconChevronDown } from '@/ui/icon/index';
type ButtonProps = React.ComponentProps<'button'>;
export type DropdownOptionType = {
key: string;
label: string;
icon: React.ReactNode;
};
type OwnProps = {
options: DropdownOptionType[];
selectedOptionKey?: string;
onSelection: (value: DropdownOptionType) => void;
} & ButtonProps;
const StyledButton = styled.button<ButtonProps & { isOpen: boolean }>`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-bottom-left-radius: ${({ isOpen, theme }) =>
isOpen ? 0 : theme.border.radius.sm};
border-bottom-right-radius: ${({ isOpen, theme }) =>
isOpen ? 0 : theme.border.radius.sm};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
svg {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
width: 14px;
}
`;
const StyledDropdownItem = styled.button<ButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
svg {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
width: 14px;
}
`;
const StyledDropdownContainer = styled.div`
position: relative;
`;
const StyledDropdownMenu = styled.div`
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
`;
export function DropdownButton_Deprecated({
options,
selectedOptionKey,
onSelection,
...buttonProps
}: OwnProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<
DropdownOptionType | undefined
>(undefined);
useEffect(() => {
if (selectedOptionKey) {
const option = options.find((option) => option.key === selectedOptionKey);
setSelectedOption(option);
} else {
setSelectedOption(options[0]);
}
}, [selectedOptionKey, options]);
if (!options.length) {
throw new Error('You must provide at least one option.');
}
const handleSelect =
(option: DropdownOptionType) =>
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
onSelection(option);
setSelectedOption(option);
setIsOpen(false);
};
return (
<>
{selectedOption && (
<StyledDropdownContainer>
<StyledButton
onClick={() => setIsOpen(!isOpen)}
{...buttonProps}
isOpen={isOpen}
>
{selectedOption.icon}
{selectedOption.label}
{options.length > 1 && <IconChevronDown />}
</StyledButton>
{isOpen && (
<StyledDropdownMenu>
{options
.filter((option) => option.label !== selectedOption.label)
.map((option, index) => (
<StyledDropdownItem
key={index}
onClick={handleSelect(option)}
>
{option.icon}
{option.label}
</StyledDropdownItem>
))}
</StyledDropdownMenu>
)}
</StyledDropdownContainer>
)}
</>
);
}

View File

@ -0,0 +1,113 @@
import React, { useMemo } from 'react';
import styled from '@emotion/styled';
import { TablerIconsProps } from '@tabler/icons-react';
export type FloatingButtonSize = 'small' | 'medium';
export type FloatingButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
export type FloatingButtonProps = {
className?: string;
icon?: React.ReactNode;
title?: string;
size?: FloatingButtonSize;
position?: FloatingButtonPosition;
applyShadow?: boolean;
applyBlur?: boolean;
disabled?: boolean;
focus?: boolean;
};
const StyledButton = styled.button<
Pick<
FloatingButtonProps,
'size' | 'focus' | 'position' | 'applyBlur' | 'applyShadow'
>
>`
align-items: center;
backdrop-filter: ${({ applyBlur }) => (applyBlur ? 'blur(20px)' : 'none')};
background: ${({ theme }) => theme.background.primary};
border: ${({ focus, theme }) =>
focus ? `1px solid ${theme.color.blue}` : 'none'};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme, applyShadow, focus }) =>
applyShadow
? `0px 2px 4px 0px ${
theme.background.transparent.light
}, 0px 0px 4px 0px ${theme.background.transparent.medium}${
focus ? `,0 0 0 3px ${theme.color.blue10}` : ''
}`
: focus
? `0 0 0 3px ${theme.color.blue10}`
: 'none'};
color: ${({ theme, disabled, focus }) => {
return !disabled
? focus
? theme.color.blue
: theme.font.color.secondary
: theme.font.color.extraLight;
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
padding: ${({ theme }) => {
return `0 ${theme.spacing(2)}`;
}};
transition: background 0.1s ease;
white-space: nowrap;
&:hover {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.lighter : 'transparent'};
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
}
&:focus {
outline: none;
}
`;
export function FloatingButton({
className,
icon: initialIcon,
title,
size = 'small',
applyBlur = true,
applyShadow = true,
disabled = false,
focus = false,
}: FloatingButtonProps) {
const icon = useMemo(() => {
if (!initialIcon || !React.isValidElement(initialIcon)) {
return null;
}
return React.cloneElement<TablerIconsProps>(initialIcon as any, {
size: 14,
});
}, [initialIcon]);
return (
<StyledButton
disabled={disabled}
focus={focus && !disabled}
size={size}
applyBlur={applyBlur}
applyShadow={applyShadow}
className={className}
>
{icon}
{title}
</StyledButton>
);
}

View File

@ -0,0 +1,50 @@
import React from 'react';
import styled from '@emotion/styled';
import { FloatingButtonPosition, FloatingButtonProps } from './FloatingButton';
const StyledFloatingButtonGroupContainer = styled.div`
backdrop-filter: blur(20px);
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) =>
`0px 2px 4px 0px ${theme.background.transparent.light}, 0px 0px 4px 0px ${theme.background.transparent.medium}`};
display: flex;
`;
export type FloatingButtonGroupProps = Pick<FloatingButtonProps, 'size'> & {
children: React.ReactElement[];
};
export function FloatingButtonGroup({
children,
size,
}: FloatingButtonGroupProps) {
return (
<StyledFloatingButtonGroupContainer>
{React.Children.map(children, (child, index) => {
let position: FloatingButtonPosition;
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
const additionalProps: any = {
position,
size,
applyShadow: false,
applyBlur: false,
};
if (size) {
additionalProps.size = size;
}
return React.cloneElement(child, additionalProps);
})}
</StyledFloatingButtonGroupContainer>
);
}

View File

@ -0,0 +1,127 @@
import React, { useMemo } from 'react';
import styled from '@emotion/styled';
import { TablerIconsProps } from '@tabler/icons-react';
export type FloatingIconButtonSize = 'small' | 'medium';
export type FloatingIconButtonPosition =
| 'standalone'
| 'left'
| 'middle'
| 'right';
export type FloatingIconButtonProps = {
className?: string;
icon?: React.ReactNode;
size?: FloatingIconButtonSize;
position?: FloatingIconButtonPosition;
applyShadow?: boolean;
applyBlur?: boolean;
disabled?: boolean;
focus?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
const StyledButton = styled.button<
Pick<
FloatingIconButtonProps,
'size' | 'position' | 'applyShadow' | 'applyBlur' | 'focus'
>
>`
align-items: center;
backdrop-filter: ${({ applyBlur }) => (applyBlur ? 'blur(20px)' : 'none')};
background: ${({ theme }) => theme.background.primary};
border: ${({ focus, theme }) =>
focus ? `1px solid ${theme.color.blue}` : 'transparent'};
border-radius: ${({ position, theme }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
}
}};
box-shadow: ${({ theme, applyShadow, focus }) =>
applyShadow
? `0px 2px 4px ${theme.background.transparent.light}, 0px 0px 4px ${
theme.background.transparent.medium
}${focus ? `,0 0 0 3px ${theme.color.blue10}` : ''}`
: focus
? `0 0 0 3px ${theme.color.blue10}`
: 'none'};
color: ${({ theme, disabled, focus }) => {
return !disabled
? focus
? theme.color.blue
: theme.font.color.tertiary
: theme.font.color.extraLight;
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: center;
padding: 0;
transition: background 0.1s ease;
white-space: nowrap;
width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
&:hover {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.lighter : 'transparent'};
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
}
&:focus {
outline: none;
}
`;
export function FloatingIconButton({
className,
icon: initialIcon,
size = 'small',
position = 'standalone',
applyShadow = true,
applyBlur = true,
disabled = false,
focus = false,
onClick,
}: FloatingIconButtonProps) {
const icon = useMemo(() => {
if (!initialIcon || !React.isValidElement(initialIcon)) {
return null;
}
return React.cloneElement<TablerIconsProps>(initialIcon as any, {
size: 16,
});
}, [initialIcon]);
return (
<StyledButton
disabled={disabled}
focus={focus && !disabled}
size={size}
applyShadow={applyShadow}
applyBlur={applyBlur}
className={className}
position={position}
onClick={onClick}
>
{icon}
</StyledButton>
);
}

View File

@ -0,0 +1,56 @@
import React from 'react';
import styled from '@emotion/styled';
import {
FloatingIconButtonPosition,
FloatingIconButtonProps,
} from './FloatingIconButton';
const StyledFloatingIconButtonGroupContainer = styled.div`
backdrop-filter: blur(20px);
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) =>
`0px 2px 4px 0px ${theme.background.transparent.light}, 0px 0px 4px 0px ${theme.background.transparent.medium}`};
display: flex;
`;
export type FloatingIconButtonGroupProps = Pick<
FloatingIconButtonProps,
'size'
> & {
children: React.ReactElement[];
};
export function FloatingIconButtonGroup({
children,
size,
}: FloatingIconButtonGroupProps) {
return (
<StyledFloatingIconButtonGroupContainer>
{React.Children.map(children, (child, index) => {
let position: FloatingIconButtonPosition;
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
const additionalProps: any = {
position,
size,
applyShadow: false,
applyBlur: false,
};
if (size) {
additionalProps.size = size;
}
return React.cloneElement(child, additionalProps);
})}
</StyledFloatingIconButtonGroupContainer>
);
}

View File

@ -1,143 +1,307 @@
import React from 'react';
import React, { useMemo } from 'react';
import styled from '@emotion/styled';
import { TablerIconsProps } from '@tabler/icons-react';
export type IconButtonVariant = 'transparent' | 'border' | 'shadow' | 'white';
export type IconButtonSize = 'medium' | 'small';
export type IconButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
export type IconButtonVariant = 'primary' | 'secondary' | 'tertiary';
export type IconButtonAccent = 'default' | 'blue' | 'danger';
export type IconButtonSize = 'large' | 'medium' | 'small';
export type IconButtonFontColor =
| 'primary'
| 'secondary'
| 'tertiary'
| 'danger';
export type ButtonProps = {
export type IconButtonProps = {
className?: string;
icon?: React.ReactNode;
variant?: IconButtonVariant;
size?: IconButtonSize;
textColor?: IconButtonFontColor;
} & React.ComponentProps<'button'>;
position?: IconButtonPosition;
accent?: IconButtonAccent;
disabled?: boolean;
focus?: boolean;
dataTestId?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
const StyledIconButton = styled.button<
Pick<ButtonProps, 'variant' | 'size' | 'textColor'>
const StyledButton = styled.button<
Pick<IconButtonProps, 'variant' | 'size' | 'position' | 'accent' | 'focus'>
>`
align-items: center;
background: ${({ theme, variant }) => {
${({ theme, variant, accent, disabled, focus }) => {
switch (variant) {
case 'shadow':
case 'white':
return theme.background.transparent.primary;
case 'transparent':
case 'border':
default:
return 'transparent';
case 'primary':
switch (accent) {
case 'default':
return `
background: ${theme.background.secondary};
border-color: ${
!disabled
? focus
? theme.color.blue
: theme.background.transparent.light
: 'transparent'
};
color: ${
!disabled
? theme.font.color.secondary
: theme.font.color.extraLight
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'
};
&:hover {
background: ${
!disabled
? theme.background.tertiary
: theme.background.secondary
};
}
&:active {
background: ${
!disabled
? theme.background.quaternary
: theme.background.secondary
};
}
`;
case 'blue':
return `
background: ${!disabled ? theme.color.blue : theme.color.blue20};
border-color: ${
!disabled
? focus
? theme.color.blue
: theme.background.transparent.light
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
color: ${theme.grayScale.gray0};
box-shadow: ${
!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'
};
&:hover {
background: ${
!disabled ? theme.color.blue50 : theme.color.blue20
};
}
&:active {
background: ${
!disabled ? theme.color.blue60 : theme.color.blue20
};
}
`;
case 'danger':
return `
background: ${!disabled ? theme.color.red : theme.color.red20};
border-color: ${
!disabled
? focus
? theme.color.red
: theme.background.transparent.light
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus ? `0 0 0 3px ${theme.color.red10}` : 'none'
};
color: ${theme.grayScale.gray0};
&:hover {
background: ${
!disabled ? theme.color.red50 : theme.color.red20
};
}
&:active {
background: ${
!disabled ? theme.color.red50 : theme.color.red20
};
}
`;
}
break;
case 'secondary':
case 'tertiary':
switch (accent) {
case 'default':
return `
background: ${
focus ? theme.background.transparent.primary : 'transparent'
};
border-color: ${
variant === 'secondary'
? !disabled && focus
? theme.color.blue
: theme.background.transparent.light
: focus
? theme.color.blue
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'
};
color: ${
!disabled
? theme.font.color.secondary
: theme.font.color.extraLight
};
&:hover {
background: ${
!disabled ? theme.background.transparent.light : 'transparent'
};
}
&:active {
background: ${
!disabled ? theme.background.transparent.light : 'transparent'
};
}
`;
case 'blue':
return `
background: ${
focus ? theme.background.transparent.primary : 'transparent'
};
border-color: ${
variant === 'secondary'
? !disabled
? theme.color.blue
: theme.color.blue20
: focus
? theme.color.blue
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus
? `0 0 0 3px ${theme.accent.tertiary}`
: 'none'
};
color: ${!disabled ? theme.color.blue : theme.accent.accent4060};
&:hover {
background: ${
!disabled ? theme.accent.tertiary : 'transparent'
};
}
&:active {
background: ${
!disabled ? theme.accent.secondary : 'transparent'
};
}
`;
case 'danger':
return `
background: ${
!disabled ? theme.background.transparent.primary : 'transparent'
};
border-color: ${
variant === 'secondary'
? !disabled
? theme.color.red
: theme.color.red20
: focus
? theme.color.red
: 'transparent'
};
border-width: ${!disabled && focus ? '1px 1px !important' : 0};
box-shadow: ${
!disabled && focus ? `0 0 0 3px ${theme.color.red10}` : 'none'
};
color: ${!disabled ? theme.font.color.danger : theme.color.red20};
&:hover {
background: ${
!disabled ? theme.background.danger : 'transparent'
};
}
&:active {
background: ${
!disabled ? theme.background.danger : 'transparent'
};
}
`;
}
}
}};
border-color: ${({ theme, variant }) => {
switch (variant) {
case 'border':
return theme.border.color.medium;
case 'shadow':
case 'white':
case 'transparent':
default:
return 'none';
}}
border-radius: ${({ position, theme }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
}
}};
border-radius: ${({ theme }) => {
return theme.border.radius.sm;
}};
border-style: solid;
border-width: ${({ variant }) => {
border-width: ${({ variant, position }) => {
switch (variant) {
case 'border':
return '1px';
case 'shadow':
case 'white':
case 'transparent':
default:
return 0;
case 'primary':
case 'secondary':
return position === 'middle' ? '1px 0px' : '1px';
case 'tertiary':
return '0';
}
}};
box-shadow: ${({ theme, variant }) => {
switch (variant) {
case 'shadow':
return theme.boxShadow.light;
case 'border':
case 'white':
case 'transparent':
default:
return 'none';
}
}};
color: ${({ theme, disabled, textColor }) => {
if (disabled) {
return theme.font.color.extraLight;
}
return textColor === 'danger'
? theme.color.red
: theme.font.color[textColor ?? 'secondary'];
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-shrink: 0;
height: ${({ size }) => {
switch (size) {
case 'large':
return '32px';
case 'medium':
return '24px';
case 'small':
default:
return '20px';
}
}};
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: 500;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: center;
outline: none;
padding: 0;
transition: background 0.1s ease;
&:hover {
background: ${({ theme, disabled }) => {
return disabled ? 'auto' : theme.background.transparent.light;
}};
white-space: nowrap;
width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
&:focus {
outline: none;
}
user-select: none;
&:active {
background: ${({ theme, disabled }) => {
return disabled ? 'auto' : theme.background.transparent.medium;
}};
}
width: ${({ size }) => {
switch (size) {
case 'large':
return '32px';
case 'medium':
return '24px';
case 'small':
default:
return '20px';
}
}};
`;
export function IconButton({
icon,
variant = 'transparent',
className,
icon: initialIcon,
variant = 'primary',
size = 'medium',
textColor = 'tertiary',
accent = 'default',
position = 'standalone',
disabled = false,
...props
}: ButtonProps) {
focus = false,
dataTestId,
onClick,
}: IconButtonProps) {
const icon = useMemo(() => {
if (!initialIcon || !React.isValidElement(initialIcon)) {
return <></>;
}
return React.cloneElement<TablerIconsProps>(initialIcon as any, {
size: 16,
});
}, [initialIcon]);
return (
<StyledIconButton
<StyledButton
data-testid={dataTestId}
variant={variant}
size={size}
position={position}
disabled={disabled}
textColor={textColor}
{...props}
focus={focus}
accent={accent}
className={className}
onClick={onClick}
>
{icon}
</StyledIconButton>
</StyledButton>
);
}

View File

@ -1,39 +1,55 @@
import React, { type ComponentProps } from 'react';
import React from 'react';
import styled from '@emotion/styled';
import type { IconButtonSize, IconButtonVariant } from './IconButton';
import { IconButtonPosition, IconButtonProps } from './IconButton';
const StyledIconButtonGroupContainer = styled.div`
align-items: flex-start;
background: ${({ theme }) => theme.background.transparent.primary};
border-radius: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
gap: ${({ theme }) => theme.spacing(0.5)};
padding: ${({ theme }) => theme.spacing(0.5)};
`;
export type IconButtonGroupProps = Omit<ComponentProps<'div'>, 'children'> & {
variant: IconButtonVariant;
size: IconButtonSize;
children: React.ReactElement | React.ReactElement[];
export type IconButtonGroupProps = Pick<
IconButtonProps,
'variant' | 'size' | 'accent'
> & {
children: React.ReactElement[];
};
export function IconButtonGroup({
children,
variant,
size,
...props
accent,
}: IconButtonGroupProps) {
return (
<StyledIconButtonGroupContainer {...props}>
{React.Children.map(
Array.isArray(children) ? children : [children],
(child) =>
React.cloneElement(child, {
...(variant ? { variant } : {}),
...(size ? { size } : {}),
}),
)}
<StyledIconButtonGroupContainer>
{React.Children.map(children, (child, index) => {
let position: IconButtonPosition;
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
const additionalProps: any = { position };
if (variant) {
additionalProps.variant = variant;
}
if (accent) {
additionalProps.accent = accent;
}
if (size) {
additionalProps.size = size;
}
return React.cloneElement(child, additionalProps);
})}
</StyledIconButtonGroupContainer>
);
}

View File

@ -0,0 +1,109 @@
import React, { MouseEvent, useMemo } from 'react';
import styled from '@emotion/styled';
import { TablerIconsProps } from '@tabler/icons-react';
export type LightButtonAccent = 'secondary' | 'tertiary';
export type LightButtonProps = {
className?: string;
icon?: React.ReactNode;
title?: string;
accent?: LightButtonAccent;
active?: boolean;
disabled?: boolean;
focus?: boolean;
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
};
const StyledButton = styled.button<
Pick<LightButtonProps, 'accent' | 'active' | 'focus'>
>`
align-items: center;
background: transparent;
border: ${({ theme, focus }) =>
focus ? `1px solid ${theme.color.blue}` : 'none'};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme, focus }) =>
focus ? `0 0 0 3px ${theme.color.blue10}` : 'none'};
color: ${({ theme, accent, active, disabled, focus }) => {
switch (accent) {
case 'secondary':
return active || focus
? theme.color.blue
: !disabled
? theme.font.color.secondary
: theme.font.color.extraLight;
case 'tertiary':
return active || focus
? theme.color.blue
: !disabled
? theme.font.color.tertiary
: theme.font.color.extraLight;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
height: 24px;
padding: ${({ theme }) => {
return `0 ${theme.spacing(2)}`;
}};
transition: background 0.1s ease;
white-space: nowrap;
&:hover {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.light : 'transparent'};
}
&:focus {
outline: none;
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
}
`;
export function LightButton({
className,
icon: initialIcon,
title,
active = false,
accent = 'secondary',
disabled = false,
focus = false,
onClick,
}: LightButtonProps) {
const icon = useMemo(() => {
if (!initialIcon || !React.isValidElement(initialIcon)) {
return null;
}
return React.cloneElement<TablerIconsProps>(initialIcon as any, {
size: 14,
});
}, [initialIcon]);
return (
<StyledButton
onClick={onClick}
disabled={disabled}
focus={focus && !disabled}
accent={accent}
className={className}
active={active}
>
{icon}
{title}
</StyledButton>
);
}

View File

@ -0,0 +1,112 @@
import React, { MouseEvent, useMemo } from 'react';
import styled from '@emotion/styled';
import { TablerIconsProps } from '@tabler/icons-react';
export type LightIconButtonAccent = 'secondary' | 'tertiary';
export type LightIconButtonSize = 'small' | 'medium';
export type LightIconButtonProps = {
className?: string;
icon?: React.ReactNode;
title?: string;
size?: LightIconButtonSize;
accent?: LightIconButtonAccent;
active?: boolean;
disabled?: boolean;
focus?: boolean;
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
};
const StyledButton = styled.button<
Pick<LightIconButtonProps, 'accent' | 'active' | 'size' | 'focus'>
>`
align-items: center;
background: transparent;
border: none;
border: ${({ disabled, theme, focus }) =>
!disabled && focus ? `1px solid ${theme.color.blue}` : 'none'};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ disabled, theme, focus }) =>
!disabled && focus ? `0 0 0 3px ${theme.color.blue10}` : 'none'};
color: ${({ theme, accent, active, disabled, focus }) => {
switch (accent) {
case 'secondary':
return active || focus
? theme.color.blue
: !disabled
? theme.font.color.secondary
: theme.font.color.extraLight;
case 'tertiary':
return active || focus
? theme.color.blue
: !disabled
? theme.font.color.tertiary
: theme.font.color.extraLight;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: center;
padding: 0;
transition: background 0.1s ease;
white-space: nowrap;
width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
&:hover {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.light : 'transparent'};
}
&:focus {
outline: none;
}
&:active {
background: ${({ theme, disabled }) =>
!disabled ? theme.background.transparent.medium : 'transparent'};
}
`;
export function LightIconButton({
className,
icon: initialIcon,
active = false,
size = 'small',
accent = 'secondary',
disabled = false,
focus = false,
onClick,
}: LightIconButtonProps) {
const icon = useMemo(() => {
if (!initialIcon || !React.isValidElement(initialIcon)) {
return null;
}
return React.cloneElement<TablerIconsProps>(initialIcon as any, {
size: 16,
});
}, [initialIcon]);
return (
<StyledButton
onClick={onClick}
disabled={disabled}
focus={focus && !disabled}
accent={accent}
className={className}
size={size}
active={active}
>
{icon}
</StyledButton>
);
}

View File

@ -1,89 +1,106 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconSearch } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Button, ButtonPosition, ButtonSize, ButtonVariant } from '../Button';
import {
Button,
ButtonAccent,
ButtonPosition,
ButtonSize,
ButtonVariant,
} from '../Button';
const meta: Meta<typeof Button> = {
title: 'UI/Button/Button',
component: Button,
argTypes: {
icon: {
type: 'boolean',
mapping: {
true: <IconSearch size={14} />,
false: undefined,
},
},
position: {
control: 'radio',
options: [undefined, ...Object.values(ButtonPosition)],
},
},
args: { title: 'Lorem ipsum' },
};
export default meta;
type Story = StoryObj<typeof Button>;
const clickJestFn = jest.fn();
export const Default: Story = {
args: { onClick: clickJestFn },
decorators: [ComponentDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
args: {
title: 'Button',
size: 'small',
variant: 'primary',
accent: 'danger',
disabled: false,
focus: false,
fullWidth: false,
soon: false,
position: 'standalone',
icon: <IconSearch />,
className: '',
},
decorators: [ComponentDecorator],
};
export const Sizes: Story = {
export const Catalog: Story = {
args: { title: 'Filter', icon: <IconSearch /> },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
focus: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: Object.values(ButtonSize),
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const Variants: Story = {
argTypes: {
disabled: { control: false },
variant: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'state',
values: ['default', 'disabled', 'hover', 'active', 'focus'],
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
if (state === 'disabled') return { disabled: true };
if (state === 'default') return {};
return { className: state };
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies ButtonAccent[],
props: (accent: ButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: Object.values(ButtonVariant),
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
@ -92,18 +109,71 @@ export const Variants: Story = {
decorators: [CatalogDecorator],
};
export const Positions: Story = {
export const SoonCatalog: Story = {
args: { title: 'Filter', icon: <IconSearch />, soon: true },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
focus: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'positions',
values: ['none', ...Object.values(ButtonPosition)],
props: (position: ButtonPosition | 'none') =>
position === 'none' ? {} : { position },
name: 'sizes',
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies ButtonAccent[],
props: (accent: ButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
@ -111,20 +181,95 @@ export const Positions: Story = {
decorators: [CatalogDecorator],
};
export const WithAdornments: Story = {
export const PositionCatalog: Story = {
args: { title: 'Filter', icon: <IconSearch /> },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
focus: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'adornments',
values: ['with icon', 'with soon pill'],
props: (value: string) =>
value === 'with icon'
? { icon: <IconSearch size={14} /> }
: { soon: true },
name: 'positions',
values: [
'standalone',
'left',
'middle',
'right',
] satisfies ButtonPosition[],
props: (position: ButtonPosition) => ({ position }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'sizes',
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const FullWidth: Story = {
args: { title: 'Filter', icon: <IconSearch />, fullWidth: true },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
focus: { control: false },
disabled: { control: false },
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
className: { control: false },
icon: { control: false },
},
decorators: [ComponentDecorator],
};

View File

@ -1,36 +1,76 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Button, ButtonPosition } from '../Button';
import { Button, ButtonAccent, ButtonSize, ButtonVariant } from '../Button';
import { ButtonGroup } from '../ButtonGroup';
const clickJestFn = jest.fn();
const meta: Meta<typeof ButtonGroup> = {
title: 'UI/Button/ButtonGroup',
component: ButtonGroup,
decorators: [ComponentDecorator],
argTypes: { children: { control: false } },
args: {
children: Object.values(ButtonPosition).map((position) => (
<Button title={position} onClick={clickJestFn} />
)),
},
};
export default meta;
type Story = StoryObj<typeof ButtonGroup>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const leftButton = canvas.getByRole('button', { name: 'left' });
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(leftButton);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
args: {
size: 'small',
variant: 'primary',
accent: 'danger',
children: [
<Button icon={<IconNotes />} title="Note" />,
<Button icon={<IconCheckbox />} title="Task" />,
<Button icon={<IconTimelineEvent />} title="Activity" />,
],
},
argTypes: {
children: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: {
children: [
<Button icon={<IconNotes />} title="Note" />,
<Button icon={<IconCheckbox />} title="Task" />,
<Button icon={<IconTimelineEvent />} title="Activity" />,
],
},
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
children: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }),
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies ButtonAccent[],
props: (accent: ButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,83 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconSearch } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FloatingButton, FloatingButtonSize } from '../FloatingButton';
const meta: Meta<typeof FloatingButton> = {
title: 'UI/Button/FloatingButton',
component: FloatingButton,
};
export default meta;
type Story = StoryObj<typeof FloatingButton>;
export const Default: Story = {
args: {
title: 'Filter',
size: 'small',
disabled: false,
focus: false,
applyBlur: true,
applyShadow: true,
position: 'standalone',
icon: <IconSearch />,
},
argTypes: {
icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { title: 'Filter', icon: <IconSearch /> },
argTypes: {
size: { control: false },
disabled: { control: false },
position: { control: false },
focus: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies FloatingButtonSize[],
props: (size: FloatingButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'disabled+focus':
return { disabled: true, focus: true };
default:
return {};
}
},
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,58 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FloatingButton, FloatingButtonSize } from '../FloatingButton';
import { FloatingButtonGroup } from '../FloatingButtonGroup';
const meta: Meta<typeof FloatingButtonGroup> = {
title: 'UI/Button/FloatingButtonGroup',
component: FloatingButtonGroup,
};
export default meta;
type Story = StoryObj<typeof FloatingButtonGroup>;
export const Default: Story = {
args: {
size: 'small',
children: [
<FloatingButton icon={<IconNotes />} />,
<FloatingButton icon={<IconCheckbox />} />,
<FloatingButton icon={<IconTimelineEvent />} />,
],
},
argTypes: {
children: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: {
children: [
<FloatingButton icon={<IconNotes />} />,
<FloatingButton icon={<IconCheckbox />} />,
<FloatingButton icon={<IconTimelineEvent />} />,
],
},
argTypes: {
size: { control: false },
children: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies FloatingButtonSize[],
props: (size: FloatingButtonSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,84 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconSearch } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import {
FloatingIconButton,
FloatingIconButtonSize,
} from '../FloatingIconButton';
const meta: Meta<typeof FloatingIconButton> = {
title: 'UI/Button/FloatingIconButton',
component: FloatingIconButton,
};
export default meta;
type Story = StoryObj<typeof FloatingIconButton>;
export const Default: Story = {
args: {
size: 'small',
disabled: false,
focus: false,
applyBlur: true,
applyShadow: true,
position: 'standalone',
icon: <IconSearch />,
},
argTypes: {
icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { icon: <IconSearch /> },
argTypes: {
size: { control: false },
disabled: { control: false },
focus: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies FloatingIconButtonSize[],
props: (size: FloatingIconButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'disabled+focus':
return { disabled: true, focus: true };
default:
return {};
}
},
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,61 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import {
FloatingIconButton,
FloatingIconButtonSize,
} from '../FloatingIconButton';
import { FloatingIconButtonGroup } from '../FloatingIconButtonGroup';
const meta: Meta<typeof FloatingIconButtonGroup> = {
title: 'UI/Button/FloatingIconButtonGroup',
component: FloatingIconButtonGroup,
};
export default meta;
type Story = StoryObj<typeof FloatingIconButtonGroup>;
export const Default: Story = {
args: {
size: 'small',
children: [
<FloatingIconButton icon={<IconNotes />} />,
<FloatingIconButton icon={<IconCheckbox />} />,
<FloatingIconButton icon={<IconTimelineEvent />} />,
],
},
argTypes: {
children: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: {
children: [
<FloatingIconButton icon={<IconNotes />} />,
<FloatingIconButton icon={<IconCheckbox />} />,
<FloatingIconButton icon={<IconTimelineEvent />} />,
],
},
argTypes: {
size: { control: false },
children: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies FloatingIconButtonSize[],
props: (size: FloatingIconButtonSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,156 +1,179 @@
import styled from '@emotion/styled';
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconUser } from '@/ui/icon';
import { IconSearch } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { IconButton } from '../IconButton';
type IconButtonProps = React.ComponentProps<typeof IconButton>;
const StyledContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 20px;
width: 800px;
> * + * {
margin-top: ${({ theme }) => theme.spacing(4)};
}
`;
const StyledTitle = styled.h1`
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledDescription = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-align: center;
text-transform: uppercase;
`;
const StyledLine = styled.div`
display: flex;
flex: 1;
flex-direction: row;
`;
const StyledIconButtonContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
width: 50px;
`;
import {
IconButton,
IconButtonAccent,
IconButtonPosition,
IconButtonSize,
IconButtonVariant,
} from '../IconButton';
const meta: Meta<typeof IconButton> = {
title: 'UI/Button/IconButton',
component: IconButton,
decorators: [
(Story) => (
<StyledContainer>
<Story />
</StyledContainer>
),
],
argTypes: { icon: { control: false }, variant: { control: false } },
};
export default meta;
type Story = StoryObj<typeof IconButton>;
const variants: IconButtonProps['variant'][] = [
'transparent',
'border',
'shadow',
'white',
];
const clickJestFn = jest.fn();
const states = {
default: {
description: 'Default',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-default`,
onClick: clickJestFn,
}),
},
hover: {
description: 'Hover',
extraProps: (variant: string) => ({
id: `${variant}-button-hover`,
'data-testid': `${variant}-button-hover`,
}),
},
pressed: {
description: 'Pressed',
extraProps: (variant: string) => ({
id: `${variant}-button-pressed`,
'data-testid': `${variant}-button-pressed`,
}),
},
disabled: {
description: 'Disabled',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-disabled`,
disabled: true,
}),
export const Default: Story = {
args: {
size: 'small',
variant: 'primary',
accent: 'danger',
disabled: false,
focus: false,
position: 'standalone',
icon: <IconSearch />,
},
decorators: [ComponentDecorator],
};
export const LargeSize: Story = {
args: { size: 'large' },
render: (args) => (
<>
{variants.map((variant) => (
<div key={variant}>
<StyledTitle>{variant}</StyledTitle>
<StyledLine>
{Object.entries(states).map(
([state, { description, extraProps }]) => (
<StyledIconButtonContainer
key={`${variant}-container-${state}`}
>
<StyledDescription>{description}</StyledDescription>
<IconButton
{...args}
{...extraProps(variant ?? '')}
variant={variant}
icon={<IconUser size={args.size === 'small' ? 14 : 16} />}
/>
</StyledIconButtonContainer>
),
)}
</StyledLine>
</div>
))}
</>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByTestId('transparent-button-default');
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
export const Catalog: Story = {
args: { icon: <IconSearch /> },
argTypes: {
size: { control: false },
variant: { control: false },
focus: { control: false },
accent: { control: false },
disabled: { control: false },
icon: { control: false },
position: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'sizes',
values: ['small', 'medium'] satisfies IconButtonSize[],
props: (size: IconButtonSize) => ({ size }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies IconButtonAccent[],
props: (accent: IconButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies IconButtonVariant[],
props: (variant: IconButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const MediumSize: Story = {
...LargeSize,
args: { size: 'medium' },
};
export const SmallSize: Story = {
...LargeSize,
args: { size: 'small' },
export const PositionCatalog: Story = {
args: { icon: <IconSearch /> },
argTypes: {
size: { control: false },
variant: { control: false },
focus: { control: false },
accent: { control: false },
disabled: { control: false },
position: { control: false },
icon: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'positions',
values: [
'standalone',
'left',
'middle',
'right',
] satisfies IconButtonPosition[],
props: (position: IconButtonPosition) => ({ position }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { focus: true, disabled: true };
default:
return {};
}
},
},
{
name: 'sizes',
values: ['small', 'medium'] satisfies IconButtonSize[],
props: (size: IconButtonSize) => ({ size }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies IconButtonVariant[],
props: (variant: IconButtonVariant) => ({ variant }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,10 +1,15 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@tabler/icons-react';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { IconButton } from '../IconButton';
import { Button } from '../Button';
import {
IconButtonAccent,
IconButtonSize,
IconButtonVariant,
} from '../IconButton';
import { IconButtonGroup } from '../IconButtonGroup';
const meta: Meta<typeof IconButtonGroup> = {
@ -15,40 +20,58 @@ const meta: Meta<typeof IconButtonGroup> = {
export default meta;
type Story = StoryObj<typeof IconButtonGroup>;
const args = {
children: [
<IconButton icon={<IconBell />} />,
<IconButton icon={<IconBell />} />,
],
};
export const Default: Story = {
args,
args: {
size: 'small',
variant: 'primary',
accent: 'danger',
children: [
<Button icon={<IconNotes />} />,
<Button icon={<IconCheckbox />} />,
<Button icon={<IconTimelineEvent />} />,
],
},
argTypes: {
children: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args,
args: {
children: [
<Button icon={<IconNotes />} />,
<Button icon={<IconCheckbox />} />,
<Button icon={<IconTimelineEvent />} />,
],
},
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
children: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'variants',
values: ['transparent', 'border', 'shadow', 'white'],
props: (variant: string) => ({
variant,
}),
name: 'sizes',
values: ['small', 'medium'] satisfies IconButtonSize[],
props: (size: IconButtonSize) => ({ size }),
},
{
name: 'sizes',
values: ['large', 'medium', 'small'],
props: (size: string) => ({
size,
}),
name: 'accents',
values: ['default', 'blue', 'danger'] satisfies IconButtonAccent[],
props: (accent: IconButtonAccent) => ({ accent }),
},
{
name: 'variants',
values: [
'primary',
'secondary',
'tertiary',
] satisfies IconButtonVariant[],
props: (variant: IconButtonVariant) => ({ variant }),
},
],
},

View File

@ -0,0 +1,87 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconSearch } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { LightButton, LightButtonAccent } from '../LightButton';
const meta: Meta<typeof LightButton> = {
title: 'UI/Button/LightButton',
component: LightButton,
};
export default meta;
type Story = StoryObj<typeof LightButton>;
export const Default: Story = {
args: {
title: 'Filter',
accent: 'secondary',
disabled: false,
active: false,
focus: false,
icon: <IconSearch />,
},
argTypes: {
icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { title: 'Filter', icon: <IconSearch /> },
argTypes: {
accent: { control: false },
disabled: { control: false },
active: { control: false },
focus: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'accents',
values: ['secondary', 'tertiary'] satisfies LightButtonAccent[],
props: (accent: LightButtonAccent) => ({ accent }),
},
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'active',
'focus',
'disabled+focus',
'disabled+active',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { disabled: true, focus: true };
case 'disabled+active':
return { disabled: true, active: true };
default:
return {};
}
},
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,96 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconSearch } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import {
LightIconButton,
LightIconButtonAccent,
LightIconButtonSize,
} from '../LightIconButton';
const meta: Meta<typeof LightIconButton> = {
title: 'UI/Button/LightIconButton',
component: LightIconButton,
};
export default meta;
type Story = StoryObj<typeof LightIconButton>;
export const Default: Story = {
args: {
title: 'Filter',
accent: 'secondary',
disabled: false,
active: false,
focus: false,
icon: <IconSearch />,
},
argTypes: {
icon: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { title: 'Filter', icon: <IconSearch /> },
argTypes: {
accent: { control: false },
disabled: { control: false },
active: { control: false },
focus: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'] },
catalog: {
dimensions: [
{
name: 'states',
values: [
'default',
'hover',
'pressed',
'disabled',
'active',
'focus',
'disabled+focus',
'disabled+active',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
case 'pressed':
return { className: state };
case 'focus':
return { focus: true };
case 'disabled':
return { disabled: true };
case 'active':
return { active: true };
case 'disabled+focus':
return { disabled: true, focus: true };
case 'disabled+active':
return { disabled: true, active: true };
default:
return {};
}
},
},
{
name: 'accents',
values: ['secondary', 'tertiary'] satisfies LightIconButtonAccent[],
props: (accent: LightIconButtonAccent) => ({ accent }),
},
{
name: 'sizes',
values: ['small', 'medium'] satisfies LightIconButtonSize[],
props: (size: LightIconButtonSize) => ({ size }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -12,15 +12,15 @@ const clickJestFn = jest.fn();
const meta: Meta<typeof RoundedIconButton> = {
title: 'UI/Button/RoundedIconButton',
component: RoundedIconButton,
decorators: [ComponentDecorator],
argTypes: { icon: { control: false } },
args: { onClick: clickJestFn, icon: <IconArrowRight size={15} /> },
};
export default meta;
type Story = StoryObj<typeof RoundedIconButton>;
export const Default: Story = {
decorators: [ComponentDecorator],
argTypes: { icon: { control: false } },
args: { onClick: clickJestFn, icon: <IconArrowRight size={15} /> },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

View File

@ -2,7 +2,7 @@ import { useCallback } from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
const StyledDialogOverlay = styled(motion.div)`
align-items: center;
@ -110,12 +110,12 @@ export function Dialog({
{buttons.map((button) => (
<StyledDialogButton
key={button.title}
onClick={(e) => {
button?.onClick?.(e);
onClick={(event) => {
button?.onClick?.(event);
closeSnackbar();
}}
fullWidth={true}
variant={button.variant ?? ButtonVariant.Secondary}
variant={button.variant ?? 'secondary'}
{...button}
/>
))}

View File

@ -80,7 +80,7 @@ export function DropdownButton({
dropdownHotkeyScope,
dropdownButtonCustomHotkeyScope,
]);
console.log(dropdownComponents, isDropdownButtonOpen);
return (
<StyledContainer ref={containerRef}>
{hotkey && (

View File

@ -1,14 +1,9 @@
import { ComponentProps } from 'react';
import styled from '@emotion/styled';
import {
IconButtonGroup,
type IconButtonGroupProps,
} from '@/ui/button/components/IconButtonGroup';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { hoverBackground } from '@/ui/theme/constants/effects';
const styledIconButtonGroupClassName = 'dropdown-menu-item-actions';
export type DropdownMenuItemAccent = 'regular' | 'danger';
const StyledItem = styled.li<{ accent: DropdownMenuItemAccent }>`
@ -39,22 +34,16 @@ const StyledItem = styled.li<{ accent: DropdownMenuItemAccent }>`
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
&:hover {
.${styledIconButtonGroupClassName} {
display: flex;
}
}
`;
const StyledActions = styled(IconButtonGroup)`
const StyledActions = styled(ButtonGroup)`
display: none;
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
`;
export type DropdownMenuItemProps = ComponentProps<'li'> & {
actions?: IconButtonGroupProps['children'];
actions?: React.ReactNode[];
accent?: DropdownMenuItemAccent;
};
@ -68,11 +57,7 @@ export function DropdownMenuItem({
<StyledItem {...props} accent={accent}>
{children}
{actions && (
<StyledActions
className={styledIconButtonGroupClassName}
variant="transparent"
size="small"
>
<StyledActions variant="tertiary" size="small">
{actions}
</StyledActions>
)}

View File

@ -1,4 +1,4 @@
import { IconButton } from '@/ui/button/components/IconButton';
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
import { IconPencil } from '@/ui/icon';
import { useEditableField } from '../hooks/useEditableField';
@ -11,11 +11,10 @@ export function EditableFieldEditButton() {
}
return (
<IconButton
variant="shadow"
<FloatingIconButton
size="small"
onClick={handleClick}
icon={<IconPencil size={14} />}
icon={<IconPencil />}
data-testid="editable-field-edit-mode-container"
/>
);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { IconFileUpload, IconTrash, IconUpload } from '@/ui/icon';
const StyledContainer = styled.div`
@ -123,7 +123,7 @@ export function ImageInput({
<Button
icon={<IconUpload size={theme.icon.size.sm} />}
onClick={onUploadButtonClick}
variant={ButtonVariant.Secondary}
variant="secondary"
title="Upload"
disabled={disabled}
fullWidth
@ -131,7 +131,7 @@ export function ImageInput({
<Button
icon={<IconTrash size={theme.icon.size.sm} />}
onClick={onRemove}
variant={ButtonVariant.Secondary}
variant="secondary"
title="Remove"
disabled={!picture || disabled}
fullWidth

View File

@ -4,13 +4,12 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { LightIconButton } from '@/ui/button/components/LightIconButton';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { IconChevronLeft, IconHeart, IconPlus } from '@/ui/icon/index';
import NavCollapseButton from '@/ui/navbar/components/NavCollapseButton';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { navbarIconSize } from '../../../navbar/constants';
import { OverflowingTextWithTooltip } from '../../../tooltip/OverflowingTextWithTooltip';
import { isNavbarOpenedState } from '../../states/isNavbarOpenedState';
@ -48,10 +47,6 @@ const StyledTopBarButtonContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledBackIconButton = styled(IconButton)`
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledTopBarIconTitleContainer = styled.div`
align-items: center;
display: flex;
@ -89,10 +84,6 @@ export function PageBar({
const isNavbarOpened = useRecoilValue(isNavbarOpenedState);
const iconSize = useIsMobile()
? navbarIconSize.mobile
: navbarIconSize.desktop;
return (
<>
<StyledTopBarContainer>
@ -104,8 +95,10 @@ export function PageBar({
)}
{hasBackButton && (
<StyledTopBarButtonContainer>
<StyledBackIconButton
icon={<IconChevronLeft size={iconSize} />}
<LightIconButton
size="medium"
accent="tertiary"
icon={<IconChevronLeft />}
onClick={navigateBack}
/>
</StyledTopBarButtonContainer>
@ -117,26 +110,27 @@ export function PageBar({
</StyledTitleContainer>
</StyledTopBarIconTitleContainer>
</StyledLeftContainer>
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<StyledActionButtonsContainer>
{onFavoriteButtonClick && (
<IconButton
icon={<IconHeart size={16} />}
size="large"
size="medium"
variant="secondary"
data-testid="add-button"
textColor={isFavorite ? 'danger' : 'secondary'}
accent={isFavorite ? 'danger' : 'default'}
onClick={onFavoriteButtonClick}
variant="border"
/>
)}
{onAddButtonClick && (
<IconButton
icon={<IconPlus size={16} />}
size="large"
size="medium"
variant="secondary"
data-testid="add-button"
textColor="secondary"
accent="default"
onClick={onAddButtonClick}
variant="border"
/>
)}
{extraButtons}

View File

@ -38,10 +38,10 @@ export function ShowPageAddButton({
buttonComponents={
<IconButton
icon={<IconPlus size={16} />}
size="large"
data-testid="add-showpage-button"
textColor={'secondary'}
variant="border"
size="medium"
dataTestId="add-showpage-button"
accent="default"
variant="secondary"
onClick={toggleDropdownButton}
/>
}

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { AnimatePresence, LayoutGroup } from 'framer-motion';
import debounce from 'lodash.debounce';
import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { TextInput } from '@/ui/input/text/components/TextInput';
import { Modal } from '@/ui/modal/components/Modal';
import {
@ -32,6 +32,7 @@ const StyledConfirmationModal = styled(Modal)`
const StyledCenteredButton = styled(Button)`
justify-content: center;
margin-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledCenteredTitle = styled.div`
@ -107,21 +108,19 @@ export function ConfirmationModal({
/>
</Section>
)}
<StyledConfirmationButton
<StyledCenteredButton
onClick={onConfirmClick}
variant={ButtonVariant.Secondary}
variant="secondary"
accent="danger"
title={deleteButtonText}
disabled={!isValidValue}
fullWidth
/>
<StyledCenteredButton
onClick={() => setIsOpen(false)}
variant={ButtonVariant.Secondary}
variant="secondary"
title="Cancel"
fullWidth
style={{
marginTop: 10,
}}
/>
</StyledConfirmationModal>
</LayoutGroup>

View File

@ -5,11 +5,7 @@ import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { supportChatState } from '@/client-config/states/supportChatState';
import {
Button,
ButtonSize,
ButtonVariant,
} from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { IconHelpCircle } from '@/ui/icon';
const StyledButtonContainer = styled.div`
@ -91,8 +87,8 @@ export default function SupportChat() {
return isFrontChatLoaded ? (
<StyledButtonContainer>
<Button
variant={ButtonVariant.Tertiary}
size={ButtonSize.Small}
variant={'tertiary'}
size={'small'}
title="Support"
icon={<IconHelpCircle size={theme.icon.size.md} />}
onClick={handleSupportClick}

View File

@ -14,7 +14,7 @@ const StyledSoonPill = styled.span`
justify-content: flex-end;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
margin-left: auto;
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => `0 ${theme.spacing(2)}`};
`;
export function SoonPill() {

View File

@ -1,6 +1,6 @@
import { LightIconButton } from '@/ui/button/components/LightIconButton';
import { IconChevronsRight } from '@/ui/icon/index';
import { IconButton } from '../../button/components/IconButton';
import { useRightDrawer } from '../hooks/useRightDrawer';
export function RightDrawerTopBarCloseButton() {
@ -11,9 +11,11 @@ export function RightDrawerTopBarCloseButton() {
}
return (
<IconButton
icon={<IconChevronsRight size={16} />}
<LightIconButton
icon={<IconChevronsRight />}
onClick={handleButtonClick}
size="medium"
accent="tertiary"
/>
);
}

View File

@ -4,7 +4,8 @@ import {
} from '@tabler/icons-react';
import { useRecoilState } from 'recoil';
import { IconButton } from '../../button/components/IconButton';
import { LightIconButton } from '@/ui/button/components/LightIconButton';
import { isRightDrawerExpandedState } from '../states/isRightDrawerExpandedState';
export function RightDrawerTopBarExpandButton() {
@ -17,12 +18,14 @@ export function RightDrawerTopBarExpandButton() {
}
return (
<IconButton
<LightIconButton
size="medium"
accent="tertiary"
icon={
isRightDrawerExpanded ? (
<IconLayoutSidebarRightCollapse size={16} />
<IconLayoutSidebarRightCollapse />
) : (
<IconLayoutSidebarRightExpand size={16} />
<IconLayoutSidebarRightExpand />
)
}
onClick={handleButtonClick}

View File

@ -46,12 +46,12 @@ export const EntityTableColumnMenu = ({
{hiddenColumns.map((column) => (
<DropdownMenuItem
key={column.id}
actions={
actions={[
<IconButton
icon={<IconPlus size={theme.icon.size.sm} />}
onClick={() => onAddColumn(column.id)}
/>
}
/>,
]}
>
{column.columnIcon &&
cloneElement(column.columnIcon, {

View File

@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState } from 'recoil';
@ -64,10 +63,6 @@ const StyledAddIconButtonWrapper = styled.div`
position: relative;
`;
const StyledAddIconButton = styled(IconButton)`
border-radius: 0;
`;
const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
position: absolute;
right: 0;
@ -76,8 +71,6 @@ const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
`;
export function EntityTableHeader() {
const theme = useTheme();
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
const [columns, setColumns] = useRecoilScopedState(
tableColumnsScopedState,
@ -207,9 +200,9 @@ export function EntityTableHeader() {
<th>
{hiddenColumns.length > 0 && (
<StyledAddIconButtonWrapper>
<StyledAddIconButton
size="large"
icon={<IconPlus size={theme.icon.size.md} />}
<IconButton
size="medium"
icon={<IconPlus />}
onClick={toggleColumnMenu}
/>
{isColumnMenuOpen && (

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { IconPencil } from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { IconButton } from '@/ui/button/components/IconButton';
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
@ -114,8 +114,7 @@ export function EditableCell({
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<IconButton
variant="shadow"
<FloatingIconButton
size="small"
onClick={handlePenClick}
icon={<IconPencil size={14} />}

View File

@ -109,20 +109,22 @@ export function TableOptionsDropdownContent({
const renderFieldActions = useCallback(
(column: ViewFieldDefinition<ViewFieldMetadata>) =>
// Do not allow hiding last visible column
!column.isVisible || visibleColumns.length > 1 ? (
<IconButton
icon={
column.isVisible ? (
<IconMinus size={theme.icon.size.sm} />
) : (
<IconPlus size={theme.icon.size.sm} />
)
}
onClick={() =>
handleColumnVisibilityChange(column.id, !column.isVisible)
}
/>
) : undefined,
!column.isVisible || visibleColumns.length > 1
? [
<IconButton
icon={
column.isVisible ? (
<IconMinus size={theme.icon.size.sm} />
) : (
<IconPlus size={theme.icon.size.sm} />
)
}
onClick={() =>
handleColumnVisibilityChange(column.id, !column.isVisible)
}
/>,
]
: undefined,
[handleColumnVisibilityChange, theme.icon.size.sm, visibleColumns.length],
);

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { Button, ButtonSize } from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
@ -136,7 +136,7 @@ export const TableUpdateViewButtonGroup = ({
return (
<StyledContainer>
<ButtonGroup size={ButtonSize.Small}>
<ButtonGroup size="small">
<Button
title="Update view"
disabled={
@ -146,7 +146,7 @@ export const TableUpdateViewButtonGroup = ({
onClick={handleViewSubmit}
/>
<Button
size={ButtonSize.Small}
size="small"
icon={<IconChevronDown />}
onClick={handleArrowDownButtonClick}
/>

View File

@ -5,6 +5,8 @@ export const accentLight = {
secondary: color.blueAccent20,
tertiary: color.blueAccent15,
quaternary: color.blueAccent10,
accent3570: color.blueAccent35,
accent4060: color.blueAccent40,
};
export const accentDark = {
@ -12,4 +14,6 @@ export const accentDark = {
secondary: color.blueAccent80,
tertiary: color.blueAccent85,
quaternary: color.blueAccent90,
accent3570: color.blueAccent70,
accent4060: color.blueAccent60,
};

View File

@ -114,9 +114,13 @@ export const color = {
gray20: grayScale.gray15,
gray10: grayScale.gray10,
blueAccent90: '#141a25',
blueAccent85: '#151D2E',
blueAccent85: '#151d2e',
blueAccent80: '#152037',
blueAccent75: '#16233F',
blueAccent75: '#16233f',
blueAccent70: '#17294a',
blueAccent60: '#18356d',
blueAccent40: '#a3c0f8',
blueAccent35: '#c8d9fb',
blueAccent25: '#dae6fc',
blueAccent20: '#e2ecfd',
blueAccent15: '#edf2fe',

View File

@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { IconCopy, IconLink } from '@/ui/icon';
import { TextInput } from '@/ui/input/text/components/TextInput';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
@ -33,7 +33,7 @@ export function WorkspaceInviteLink({ inviteLink }: OwnProps) {
</StyledLinkContainer>
<Button
icon={<IconLink size={theme.icon.size.md} />}
variant={ButtonVariant.Primary}
variant={'primary'}
title="Copy link"
onClick={() => {
enqueueSnackBar('Link copied to clipboard', {

View File

@ -4,11 +4,7 @@ import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import {
Button,
ButtonSize,
ButtonVariant,
} from '@/ui/button/components/Button';
import { Button } from '@/ui/button/components/Button';
import { IconSettings, IconTrash } from '@/ui/icon';
import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer';
import { ConfirmationModal } from '@/ui/modal/components/ConfirmationModal';
@ -115,8 +111,8 @@ export function SettingsWorkspaceMembers() {
setIsConfirmationModalOpen(true);
setUserToDelete(member.user.id);
}}
variant={ButtonVariant.Tertiary}
size={ButtonSize.Small}
variant={'tertiary'}
size={'small'}
icon={<IconTrash size={theme.icon.size.md} />}
/>
</StyledButtonContainer>