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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableDat
import { setColumn } from '@/spreadsheet-import/utils/setColumn'; import { setColumn } from '@/spreadsheet-import/utils/setColumn';
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn'; import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn'; import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
import { ButtonVariant } from '@/ui/button/components/Button';
import { useDialog } from '@/ui/dialog/hooks/useDialog'; import { useDialog } from '@/ui/dialog/hooks/useDialog';
import { Modal } from '@/ui/modal/components/Modal'; import { Modal } from '@/ui/modal/components/Modal';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
@ -224,7 +223,7 @@ export const MatchColumnsStep = <T extends string>({
{ {
title: 'Continue', title: 'Continue',
onClick: handleAlertOnContinue, 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 { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import type { Data } from '@/spreadsheet-import/types'; import type { Data } from '@/spreadsheet-import/types';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; 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 { useDialog } from '@/ui/dialog/hooks/useDialog';
import { IconTrash } from '@/ui/icon'; import { IconTrash } from '@/ui/icon';
import { Toggle } from '@/ui/input/toggle/components/Toggle'; import { Toggle } from '@/ui/input/toggle/components/Toggle';
@ -173,7 +173,7 @@ export const ValidationStep = <T extends string>({
{ title: 'Cancel' }, { title: 'Cancel' },
{ {
title: 'Submit', title: 'Submit',
variant: ButtonVariant.Primary, variant: 'primary',
onClick: submitData, onClick: submitData,
}, },
], ],
@ -201,7 +201,7 @@ export const ValidationStep = <T extends string>({
<Button <Button
icon={<IconTrash />} icon={<IconTrash />}
title="Remove" title="Remove"
variant={ButtonVariant.Danger} accent="danger"
onClick={deleteSelectedRows} onClick={deleteSelectedRows}
disabled={selectedRows.size === 0} disabled={selectedRows.size === 0}
/> />

View File

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

View File

@ -8,11 +8,19 @@ const StyledButtonGroupContainer = styled.div`
display: flex; display: flex;
`; `;
type ButtonGroupProps = Pick<ButtonProps, 'variant' | 'size'> & { export type ButtonGroupProps = Pick<
ButtonProps,
'variant' | 'size' | 'accent'
> & {
children: ReactNode[]; children: ReactNode[];
}; };
export function ButtonGroup({ children, variant, size }: ButtonGroupProps) { export function ButtonGroup({
children,
variant,
size,
accent,
}: ButtonGroupProps) {
return ( return (
<StyledButtonGroupContainer> <StyledButtonGroupContainer>
{React.Children.map(children, (child, index) => { {React.Children.map(children, (child, index) => {
@ -21,19 +29,23 @@ export function ButtonGroup({ children, variant, size }: ButtonGroupProps) {
let position: ButtonPosition; let position: ButtonPosition;
if (index === 0) { if (index === 0) {
position = ButtonPosition.Left; position = 'left';
} else if (index === children.length - 1) { } else if (index === children.length - 1) {
position = ButtonPosition.Right; position = 'right';
} else { } else {
position = ButtonPosition.Middle; position = 'middle';
} }
const additionalProps: any = { position }; const additionalProps: any = { position, variant, accent, size };
if (variant) { if (variant) {
additionalProps.variant = variant; additionalProps.variant = variant;
} }
if (accent) {
additionalProps.variant = variant;
}
if (size) { if (size) {
additionalProps.size = 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 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 IconButtonProps = {
className?: string;
export type IconButtonFontColor =
| 'primary'
| 'secondary'
| 'tertiary'
| 'danger';
export type ButtonProps = {
icon?: React.ReactNode; icon?: React.ReactNode;
variant?: IconButtonVariant; variant?: IconButtonVariant;
size?: IconButtonSize; size?: IconButtonSize;
textColor?: IconButtonFontColor; position?: IconButtonPosition;
} & React.ComponentProps<'button'>; accent?: IconButtonAccent;
disabled?: boolean;
focus?: boolean;
dataTestId?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
const StyledIconButton = styled.button< const StyledButton = styled.button<
Pick<ButtonProps, 'variant' | 'size' | 'textColor'> Pick<IconButtonProps, 'variant' | 'size' | 'position' | 'accent' | 'focus'>
>` >`
align-items: center; align-items: center;
background: ${({ theme, variant }) => { ${({ theme, variant, accent, disabled, focus }) => {
switch (variant) { switch (variant) {
case 'shadow': case 'primary':
case 'white': switch (accent) {
return theme.background.transparent.primary; case 'default':
case 'transparent': return `
case 'border': background: ${theme.background.secondary};
default: border-color: ${
return 'transparent'; !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) { border-radius: ${({ position, theme }) => {
case 'border': switch (position) {
return theme.border.color.medium; case 'left':
case 'shadow': return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
case 'white': case 'right':
case 'transparent': return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
default: case 'middle':
return 'none'; return '0px';
case 'standalone':
return theme.border.radius.sm;
} }
}}; }};
border-radius: ${({ theme }) => {
return theme.border.radius.sm;
}};
border-style: solid; border-style: solid;
border-width: ${({ variant }) => { border-width: ${({ variant, position }) => {
switch (variant) { switch (variant) {
case 'border': case 'primary':
return '1px'; case 'secondary':
case 'shadow': return position === 'middle' ? '1px 0px' : '1px';
case 'white': case 'tertiary':
case 'transparent': return '0';
default:
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')}; cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex; display: flex;
flex-shrink: 0; flex-direction: row;
height: ${({ size }) => { font-family: ${({ theme }) => theme.font.family};
switch (size) { font-weight: 500;
case 'large': gap: ${({ theme }) => theme.spacing(1)};
return '32px'; height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
case 'medium':
return '24px';
case 'small':
default:
return '20px';
}
}};
justify-content: center; justify-content: center;
outline: none;
padding: 0; padding: 0;
transition: background 0.1s ease; transition: background 0.1s ease;
&:hover {
background: ${({ theme, disabled }) => { white-space: nowrap;
return disabled ? 'auto' : theme.background.transparent.light;
}}; 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({ export function IconButton({
icon, className,
variant = 'transparent', icon: initialIcon,
variant = 'primary',
size = 'medium', size = 'medium',
textColor = 'tertiary', accent = 'default',
position = 'standalone',
disabled = false, disabled = false,
...props focus = false,
}: ButtonProps) { dataTestId,
onClick,
}: IconButtonProps) {
const icon = useMemo(() => {
if (!initialIcon || !React.isValidElement(initialIcon)) {
return <></>;
}
return React.cloneElement<TablerIconsProps>(initialIcon as any, {
size: 16,
});
}, [initialIcon]);
return ( return (
<StyledIconButton <StyledButton
data-testid={dataTestId}
variant={variant} variant={variant}
size={size} size={size}
position={position}
disabled={disabled} disabled={disabled}
textColor={textColor} focus={focus}
{...props} accent={accent}
className={className}
onClick={onClick}
> >
{icon} {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 styled from '@emotion/styled';
import type { IconButtonSize, IconButtonVariant } from './IconButton'; import { IconButtonPosition, IconButtonProps } from './IconButton';
const StyledIconButtonGroupContainer = styled.div` const StyledIconButtonGroupContainer = styled.div`
align-items: flex-start; border-radius: ${({ theme }) => theme.border.radius.md};
background: ${({ theme }) => theme.background.transparent.primary};
border-radius: ${({ theme }) => theme.spacing(1)};
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(0.5)};
padding: ${({ theme }) => theme.spacing(0.5)};
`; `;
export type IconButtonGroupProps = Omit<ComponentProps<'div'>, 'children'> & { export type IconButtonGroupProps = Pick<
variant: IconButtonVariant; IconButtonProps,
size: IconButtonSize; 'variant' | 'size' | 'accent'
children: React.ReactElement | React.ReactElement[]; > & {
children: React.ReactElement[];
}; };
export function IconButtonGroup({ export function IconButtonGroup({
children, children,
variant, variant,
size, size,
...props accent,
}: IconButtonGroupProps) { }: IconButtonGroupProps) {
return ( return (
<StyledIconButtonGroupContainer {...props}> <StyledIconButtonGroupContainer>
{React.Children.map( {React.Children.map(children, (child, index) => {
Array.isArray(children) ? children : [children], let position: IconButtonPosition;
(child) =>
React.cloneElement(child, { if (index === 0) {
...(variant ? { variant } : {}), position = 'left';
...(size ? { size } : {}), } 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> </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 type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconSearch } from '@/ui/icon'; import { IconSearch } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; 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> = { const meta: Meta<typeof Button> = {
title: 'UI/Button/Button', title: 'UI/Button/Button',
component: 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; export default meta;
type Story = StoryObj<typeof Button>; type Story = StoryObj<typeof Button>;
const clickJestFn = jest.fn();
export const Default: Story = { export const Default: Story = {
args: { onClick: clickJestFn }, args: {
decorators: [ComponentDecorator], title: 'Button',
play: async ({ canvasElement }) => { size: 'small',
const canvas = within(canvasElement); variant: 'primary',
const button = canvas.getByRole('button'); accent: 'danger',
disabled: false,
const numberOfClicks = clickJestFn.mock.calls.length; focus: false,
await userEvent.click(button); fullWidth: false,
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1); soon: false,
position: 'standalone',
icon: <IconSearch />,
className: '',
}, },
decorators: [ComponentDecorator],
}; };
export const Sizes: Story = { export const Catalog: Story = {
args: { title: 'Filter', icon: <IconSearch /> },
argTypes: { argTypes: {
size: { control: false }, 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: { parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: { catalog: {
dimensions: [ dimensions: [
{ {
name: 'sizes', name: 'sizes',
values: Object.values(ButtonSize), values: ['small', 'medium'] satisfies ButtonSize[],
props: (size: ButtonSize) => ({ size }), 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', name: 'states',
values: ['default', 'disabled', 'hover', 'active', 'focus'], values: [
'default',
'hover',
'pressed',
'disabled',
'focus',
'disabled+focus',
],
props: (state: string) => { props: (state: string) => {
if (state === 'disabled') return { disabled: true }; switch (state) {
if (state === 'default') return {}; case 'default':
return { className: state }; 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', name: 'variants',
values: Object.values(ButtonVariant), values: [
'primary',
'secondary',
'tertiary',
] satisfies ButtonVariant[],
props: (variant: ButtonVariant) => ({ variant }), props: (variant: ButtonVariant) => ({ variant }),
}, },
], ],
@ -92,18 +109,71 @@ export const Variants: Story = {
decorators: [CatalogDecorator], decorators: [CatalogDecorator],
}; };
export const Positions: Story = { export const SoonCatalog: Story = {
args: { title: 'Filter', icon: <IconSearch />, soon: true },
argTypes: { 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 }, position: { control: false },
className: { control: false },
}, },
parameters: { parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: { catalog: {
dimensions: [ dimensions: [
{ {
name: 'positions', name: 'sizes',
values: ['none', ...Object.values(ButtonPosition)], values: ['small', 'medium'] satisfies ButtonSize[],
props: (position: ButtonPosition | 'none') => props: (size: ButtonSize) => ({ size }),
position === 'none' ? {} : { 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: '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], 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: { parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: { catalog: {
dimensions: [ dimensions: [
{ {
name: 'adornments', name: 'positions',
values: ['with icon', 'with soon pill'], values: [
props: (value: string) => 'standalone',
value === 'with icon' 'left',
? { icon: <IconSearch size={14} /> } 'middle',
: { soon: true }, '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], 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 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 { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Button, ButtonPosition } from '../Button'; import { Button, ButtonAccent, ButtonSize, ButtonVariant } from '../Button';
import { ButtonGroup } from '../ButtonGroup'; import { ButtonGroup } from '../ButtonGroup';
const clickJestFn = jest.fn();
const meta: Meta<typeof ButtonGroup> = { const meta: Meta<typeof ButtonGroup> = {
title: 'UI/Button/ButtonGroup', title: 'UI/Button/ButtonGroup',
component: ButtonGroup, component: ButtonGroup,
decorators: [ComponentDecorator],
argTypes: { children: { control: false } },
args: {
children: Object.values(ButtonPosition).map((position) => (
<Button title={position} onClick={clickJestFn} />
)),
},
}; };
export default meta; export default meta;
type Story = StoryObj<typeof ButtonGroup>; type Story = StoryObj<typeof ButtonGroup>;
export const Default: Story = { export const Default: Story = {
play: async ({ canvasElement }) => { args: {
const canvas = within(canvasElement); size: 'small',
const leftButton = canvas.getByRole('button', { name: 'left' }); variant: 'primary',
accent: 'danger',
const numberOfClicks = clickJestFn.mock.calls.length; children: [
await userEvent.click(leftButton); <Button icon={<IconNotes />} title="Note" />,
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1); <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 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'; import {
IconButton,
type IconButtonProps = React.ComponentProps<typeof IconButton>; IconButtonAccent,
IconButtonPosition,
const StyledContainer = styled.div` IconButtonSize,
display: flex; IconButtonVariant,
flex: 1; } from '../IconButton';
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;
`;
const meta: Meta<typeof IconButton> = { const meta: Meta<typeof IconButton> = {
title: 'UI/Button/IconButton', title: 'UI/Button/IconButton',
component: IconButton, component: IconButton,
decorators: [
(Story) => (
<StyledContainer>
<Story />
</StyledContainer>
),
],
argTypes: { icon: { control: false }, variant: { control: false } },
}; };
export default meta; export default meta;
type Story = StoryObj<typeof IconButton>; type Story = StoryObj<typeof IconButton>;
const variants: IconButtonProps['variant'][] = [ export const Default: Story = {
'transparent', args: {
'border', size: 'small',
'shadow', variant: 'primary',
'white', accent: 'danger',
]; disabled: false,
focus: false,
const clickJestFn = jest.fn(); position: 'standalone',
icon: <IconSearch />,
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,
}),
}, },
decorators: [ComponentDecorator],
}; };
export const LargeSize: Story = { export const Catalog: Story = {
args: { size: 'large' }, args: { icon: <IconSearch /> },
render: (args) => ( argTypes: {
<> size: { control: false },
{variants.map((variant) => ( variant: { control: false },
<div key={variant}> focus: { control: false },
<StyledTitle>{variant}</StyledTitle> accent: { control: false },
<StyledLine> disabled: { control: false },
{Object.entries(states).map( icon: { control: false },
([state, { description, extraProps }]) => ( position: { control: false },
<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);
}, },
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 = { export const PositionCatalog: Story = {
...LargeSize, args: { icon: <IconSearch /> },
args: { size: 'medium' }, argTypes: {
}; size: { control: false },
variant: { control: false },
export const SmallSize: Story = { focus: { control: false },
...LargeSize, accent: { control: false },
args: { size: 'small' }, 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 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 { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { IconButton } from '../IconButton'; import { Button } from '../Button';
import {
IconButtonAccent,
IconButtonSize,
IconButtonVariant,
} from '../IconButton';
import { IconButtonGroup } from '../IconButtonGroup'; import { IconButtonGroup } from '../IconButtonGroup';
const meta: Meta<typeof IconButtonGroup> = { const meta: Meta<typeof IconButtonGroup> = {
@ -15,40 +20,58 @@ const meta: Meta<typeof IconButtonGroup> = {
export default meta; export default meta;
type Story = StoryObj<typeof IconButtonGroup>; type Story = StoryObj<typeof IconButtonGroup>;
const args = {
children: [
<IconButton icon={<IconBell />} />,
<IconButton icon={<IconBell />} />,
],
};
export const Default: Story = { 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], decorators: [ComponentDecorator],
}; };
export const Catalog: Story = { export const Catalog: Story = {
args, args: {
children: [
<Button icon={<IconNotes />} />,
<Button icon={<IconCheckbox />} />,
<Button icon={<IconTimelineEvent />} />,
],
},
argTypes: { argTypes: {
size: { control: false }, size: { control: false },
variant: { control: false }, variant: { control: false },
accent: { control: false },
children: { control: false },
}, },
parameters: { parameters: {
catalog: { catalog: {
dimensions: [ dimensions: [
{ {
name: 'variants', name: 'sizes',
values: ['transparent', 'border', 'shadow', 'white'], values: ['small', 'medium'] satisfies IconButtonSize[],
props: (variant: string) => ({ props: (size: IconButtonSize) => ({ size }),
variant,
}),
}, },
{ {
name: 'sizes', name: 'accents',
values: ['large', 'medium', 'small'], values: ['default', 'blue', 'danger'] satisfies IconButtonAccent[],
props: (size: string) => ({ props: (accent: IconButtonAccent) => ({ accent }),
size, },
}), {
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> = { const meta: Meta<typeof RoundedIconButton> = {
title: 'UI/Button/RoundedIconButton', title: 'UI/Button/RoundedIconButton',
component: RoundedIconButton, component: RoundedIconButton,
decorators: [ComponentDecorator],
argTypes: { icon: { control: false } },
args: { onClick: clickJestFn, icon: <IconArrowRight size={15} /> },
}; };
export default meta; export default meta;
type Story = StoryObj<typeof RoundedIconButton>; type Story = StoryObj<typeof RoundedIconButton>;
export const Default: Story = { export const Default: Story = {
decorators: [ComponentDecorator],
argTypes: { icon: { control: false } },
args: { onClick: clickJestFn, icon: <IconArrowRight size={15} /> },
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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