Introduce accent for chips (#911)

* Introduce accent for chips

* Add top bar on Mobile on Settings pages

* Various fixes

* Fix according to peer review
This commit is contained in:
Charles Bochet
2023-07-24 16:49:33 -07:00
committed by GitHub
parent b2f4108d89
commit d6afbe8e8e
23 changed files with 166 additions and 279 deletions

View File

@ -1,60 +1,27 @@
import { useTheme } from '@emotion/react';
import {
DropdownButton,
DropdownOptionType,
} from '@/ui/button/components/DropdownButton';
import { IconCheck, IconNotes } from '@/ui/icon';
import {
ActivityType,
CommentThread,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
Chip,
ChipAccent,
ChipSize,
ChipVariant,
} from '@/ui/chip/components/Chip';
import { IconPhone } from '@/ui/icon';
import { CommentThread } from '~/generated/graphql';
type OwnProps = {
commentThread: Pick<CommentThread, 'id' | 'type'>;
commentThread: Pick<CommentThread, 'type'>;
};
export function CommentThreadTypeDropdown({ commentThread }: OwnProps) {
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
const options: DropdownOptionType[] = [
{ label: 'Note', key: 'note', icon: <IconNotes /> },
{ label: 'Task', key: 'task', icon: <IconCheck /> },
];
function getSelectedOptionKey() {
if (commentThread.type === ActivityType.Note) {
return 'note';
} else if (commentThread.type === ActivityType.Task) {
return 'task';
} else {
return undefined;
}
}
const convertSelectionOptionKeyToActivityType = (key: string) => {
switch (key) {
case 'note':
return ActivityType.Note;
case 'task':
return ActivityType.Task;
default:
return undefined;
}
};
const handleSelect = (selectedOption: DropdownOptionType) => {
updateCommentThreadMutation({
variables: {
id: commentThread.id,
type: convertSelectionOptionKeyToActivityType(selectedOption.key),
},
});
};
const theme = useTheme();
return (
<DropdownButton
options={options}
onSelection={handleSelect}
selectedOptionKey={getSelectedOptionKey()}
<Chip
label={commentThread.type}
leftComponent={<IconPhone size={theme.icon.size.md} />}
size={ChipSize.Large}
accent={ChipAccent.TextSecondary}
variant={ChipVariant.Highlighted}
/>
);
}

View File

@ -47,10 +47,6 @@ export const SmallName: Story = {
},
};
export const Clickable: Story = {
args: { ...SmallName.args, clickable: true },
};
export const BigName: Story = {
args: {
id: 'google',

View File

@ -1,9 +1,10 @@
import { PersonChip } from '@/people/components/PersonChip';
import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useEditableCell } from '@/ui/table/editable-cell/hooks/useEditableCell';
import { Company, User } from '~/generated/graphql';
import { UserChip } from '../../users/components/UserChip';
import { CompanyAccountOwnerPicker } from './CompanyAccountOwnerPicker';
export type CompanyAccountOnwer = Pick<Company, 'id'> & {
@ -38,7 +39,7 @@ export function CompanyAccountOwnerCell({ company }: OwnProps) {
}
nonEditModeContent={
company.accountOwner?.displayName ? (
<PersonChip
<UserChip
id={company.accountOwner.id}
name={company.accountOwner?.displayName ?? ''}
pictureUrl={company.accountOwner?.avatarUrl ?? ''}

View File

@ -19,6 +19,7 @@ import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedSta
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
import { EntityChipVariant } from '../../ui/chip/components/EntityChip';
import { PipelineProgressForBoard } from '../types/CompanyProgress';
import { CompanyChip } from './CompanyChip';
@ -177,7 +178,7 @@ export function CompanyBoardCard() {
id={company.id}
name={company.name}
pictureUrl={getLogoUrlFromDomainName(company.domainName)}
clickable={false}
variant={EntityChipVariant.Transparent}
/>
<StyledCheckboxContainer className="checkbox-container">
<Checkbox

View File

@ -1,25 +1,26 @@
import { EntityChip } from '@/ui/chip/components/EntityChip';
import { EntityChip, EntityChipVariant } from '@/ui/chip/components/EntityChip';
type OwnProps = {
id: string;
name: string;
pictureUrl?: string;
clickable?: boolean;
variant?: EntityChipVariant;
};
export function CompanyChip({
id,
name,
pictureUrl,
clickable = true,
variant = EntityChipVariant.Regular,
}: OwnProps) {
return (
<EntityChip
entityId={id}
linkToEntity={clickable ? `/companies/${id}` : undefined}
linkToEntity={`/companies/${id}`}
name={name}
avatarType="squared"
pictureUrl={pictureUrl}
variant={variant}
/>
);
}

View File

@ -37,7 +37,6 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
<CompanyChip
id={company.id}
name={company.name}
clickable
pictureUrl={getLogoUrlFromDomainName(company.domainName)}
/>
}

View File

@ -1,25 +1,26 @@
import { EntityChip } from '@/ui/chip/components/EntityChip';
import { EntityChip, EntityChipVariant } from '@/ui/chip/components/EntityChip';
export type PersonChipPropsType = {
id: string;
name: string;
pictureUrl?: string;
clickable?: boolean;
variant?: EntityChipVariant;
};
export function PersonChip({
id,
name,
pictureUrl,
clickable = true,
variant,
}: PersonChipPropsType) {
return (
<EntityChip
entityId={id}
linkToEntity={clickable ? `/person/${id}` : undefined}
linkToEntity={`/person/${id}`}
name={name}
avatarType="rounded"
pictureUrl={pictureUrl}
variant={variant}
/>
);
}

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 DropdownContainer = styled.div`
position: relative;
`;
const DropdownMenu = styled.div`
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
`;
export function DropdownButton({
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 && (
<DropdownContainer>
<StyledButton
onClick={() => setIsOpen(!isOpen)}
{...buttonProps}
isOpen={isOpen}
>
{selectedOption.icon}
{selectedOption.label}
{options.length > 1 && <IconChevronDown />}
</StyledButton>
{isOpen && (
<DropdownMenu>
{options
.filter((option) => option.label !== selectedOption.label)
.map((option, index) => (
<StyledDropdownItem
key={index}
onClick={handleSelect(option)}
>
{option.icon}
{option.label}
</StyledDropdownItem>
))}
</DropdownMenu>
)}
</DropdownContainer>
)}
</>
);
}

View File

@ -8,6 +8,11 @@ export enum ChipSize {
Small = 'small',
}
export enum ChipAccent {
TextPrimary = 'text-primary',
TextSecondary = 'text-secondary',
}
export enum ChipVariant {
Highlighted = 'highlighted',
Regular = 'regular',
@ -21,6 +26,7 @@ type OwnProps = {
label: string;
maxWidth?: string;
variant?: ChipVariant;
accent?: ChipAccent;
leftComponent?: React.ReactNode;
rightComponent?: React.ReactNode;
className?: string;
@ -34,14 +40,18 @@ const StyledContainer = styled.div<Partial<OwnProps>>`
? theme.background.transparent.light
: 'transparent'};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme, disabled }) =>
disabled ? theme.font.color.light : theme.font.color.primary};
color: ${({ theme, disabled, accent }) =>
disabled
? theme.font.color.light
: accent === ChipAccent.TextPrimary
? theme.font.color.primary
: theme.font.color.secondary};
cursor: ${({ clickable, disabled, variant }) =>
disabled || variant === ChipVariant.Transparent
? 'auto'
? 'inherit'
: clickable
? 'pointer'
: 'auto'};
: 'inherit'};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
@ -98,6 +108,7 @@ export function Chip({
variant = ChipVariant.Regular,
leftComponent,
rightComponent,
accent = ChipAccent.TextPrimary,
maxWidth,
className,
}: OwnProps) {
@ -106,9 +117,11 @@ export function Chip({
data-testid="chip"
clickable={clickable}
variant={variant}
accent={accent}
size={size}
disabled={disabled}
className={className}
maxWidth={maxWidth}
>
{leftComponent}
<StyledLabel>

View File

@ -12,14 +12,21 @@ type OwnProps = {
name: string;
pictureUrl?: string;
avatarType?: AvatarType;
variant?: EntityChipVariant;
};
export enum EntityChipVariant {
Regular = 'regular',
Transparent = 'transparent',
}
export function EntityChip({
linkToEntity,
entityId,
name,
pictureUrl,
avatarType = 'rounded',
variant = EntityChipVariant.Regular,
}: OwnProps) {
const navigate = useNavigate();
@ -35,7 +42,13 @@ export function EntityChip({
<div onClick={handleLinkClick}>
<Chip
label={name}
variant={linkToEntity ? ChipVariant.Highlighted : ChipVariant.Regular}
variant={
linkToEntity
? variant === EntityChipVariant.Regular
? ChipVariant.Highlighted
: ChipVariant.Regular
: ChipVariant.Transparent
}
leftComponent={
<Avatar
avatarUrl={pictureUrl}

View File

@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { ExhaustiveComponentDecorator } from '~/testing/decorators/ExhaustiveComponentDecorator';
import { Chip, ChipSize, ChipVariant } from '../Chip';
import { Chip, ChipAccent, ChipSize, ChipVariant } from '../Chip';
const meta: Meta<typeof Chip> = {
title: 'UI/Chip/Chip',
@ -18,13 +18,15 @@ export const Default: Story = {
label: 'Chip test',
size: ChipSize.Small,
variant: ChipVariant.Highlighted,
accent: ChipAccent.TextPrimary,
disabled: false,
clickable: true,
maxWidth: '200px',
},
decorators: [ComponentDecorator],
};
export const All: Story = {
export const Catalog: Story = {
args: { size: ChipSize.Large, clickable: true, label: 'Hello' },
argTypes: {
size: { control: false },
@ -42,6 +44,7 @@ export const All: Story = {
ChipVariant.Transparent,
],
sizes: [ChipSize.Small, ChipSize.Large],
accents: [ChipAccent.TextPrimary, ChipAccent.TextSecondary],
states: ['default', 'hover', 'active', 'disabled'],
},
decorators: [ExhaustiveComponentDecorator],

View File

@ -1,20 +1,29 @@
import styled from '@emotion/styled';
import { useIsMobile } from '../../hooks/useIsMobile';
import { TopBar } from '../top-bar/components/TopBar';
import { RightDrawerContainer } from './RightDrawerContainer';
type OwnProps = {
children: JSX.Element | JSX.Element[];
title: string;
icon: React.ReactNode;
};
const StyledContainer = styled.div`
const StyledContainer = styled.div<{ isMobile: boolean }>`
display: flex;
padding-top: ${({ theme }) => theme.spacing(4)};
flex-direction: column;
padding-top: ${({ theme, isMobile }) => (!isMobile ? theme.spacing(4) : 0)};
width: 100%;
`;
export function SubMenuTopBarContainer({ children }: OwnProps) {
export function SubMenuTopBarContainer({ children, title, icon }: OwnProps) {
const isMobile = useIsMobile();
return (
<StyledContainer>
<StyledContainer isMobile={isMobile}>
{isMobile && <TopBar title={title} icon={icon} />}
<RightDrawerContainer topMargin={16}>{children}</RightDrawerContainer>
</StyledContainer>
);

View File

@ -74,11 +74,8 @@ export function TopBar({
const navigate = useNavigate();
const navigateBack = useCallback(() => navigate(-1), [navigate]);
const isMobile = useIsMobile();
const isNavbarOpened = useRecoilValue(isNavbarOpenedState);
const showNavCollapseButton = isMobile || !isNavbarOpened;
const iconSize = useIsMobile()
? navbarIconSize.mobile
: navbarIconSize.desktop;
@ -87,7 +84,7 @@ export function TopBar({
<>
<TopBarContainer>
<StyledLeftContainer>
{showNavCollapseButton && (
{!isNavbarOpened && (
<TopBarButtonContainer>
<NavCollapseButton direction="right" />
</TopBarButtonContainer>

View File

@ -11,6 +11,8 @@ type OwnProps = {
const StyledClickable = styled.div`
display: flex;
overflow: hidden;
white-space: nowrap;
a {
color: inherit;

View File

@ -15,8 +15,8 @@ type OwnProps = {
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
padding-top: ${({ theme }) => theme.spacing(2)};
width: ${({ theme }) => (useIsMobile() ? '100%' : leftNavbarWidth.desktop)};
padding-top: ${({ theme }) => theme.spacing(9)};
width: ${() => (useIsMobile() ? '100%' : leftNavbarWidth.desktop)};
`;
export default function SubMenuNavbar({ children, backButtonTitle }: OwnProps) {

View File

@ -82,8 +82,6 @@ export function RightDrawer() {
: theme.rightDrawerWidth
: '0';
console.log(rightDrawerWidth);
if (!isDefined(rightDrawerPage)) {
return <></>;
}

View File

@ -0,0 +1,19 @@
import { EntityChip, EntityChipVariant } from '@/ui/chip/components/EntityChip';
export type UserChipPropsType = {
id: string;
name: string;
pictureUrl?: string;
variant?: EntityChipVariant;
};
export function UserChip({ id, name, pictureUrl, variant }: UserChipPropsType) {
return (
<EntityChip
entityId={id}
name={name}
avatarType="rounded"
pictureUrl={pictureUrl}
/>
);
}

View File

@ -24,7 +24,10 @@ export function People() {
async function handleAddButtonClick() {
await insertOnePerson({
variables: {
data: {},
data: {
firstName: '',
lastName: '',
},
},
refetchQueries: [getOperationName(GET_PEOPLE) ?? ''],
});

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { ColorSchemePicker } from '@/ui/color-scheme/components/ColorSchemePicker';
import { IconSettings } from '@/ui/icon';
import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer';
import { useColorScheme } from '@/ui/themes/hooks/useColorScheme';
import { MainSectionTitle } from '@/ui/title/components/MainSectionTitle';
@ -27,7 +28,7 @@ export function SettingsExperience() {
const { colorScheme, setColorScheme } = useColorScheme();
return (
<SubMenuTopBarContainer>
<SubMenuTopBarContainer icon={<IconSettings size={16} />} title="Settings">
<div>
<StyledContainer>
<MainSectionTitle>Experience</MainSectionTitle>

View File

@ -3,6 +3,7 @@ import styled from '@emotion/styled';
import { EmailField } from '@/settings/profile/components/EmailField';
import { NameFields } from '@/settings/profile/components/NameFields';
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
import { IconSettings } from '@/ui/icon';
import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer';
import { MainSectionTitle } from '@/ui/title/components/MainSectionTitle';
import { SubSectionTitle } from '@/ui/title/components/SubSectionTitle';
@ -26,8 +27,8 @@ const StyledSectionContainer = styled.div`
export function SettingsProfile() {
return (
<SubMenuTopBarContainer>
<div>
<SubMenuTopBarContainer icon={<IconSettings size={16} />} title="Settings">
<>
<StyledContainer>
<MainSectionTitle>Profile</MainSectionTitle>
<StyledSectionContainer>
@ -49,7 +50,7 @@ export function SettingsProfile() {
<EmailField />
</StyledSectionContainer>
</StyledContainer>
</div>
</>
</SubMenuTopBarContainer>
);
}

View File

@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { NameField } from '@/settings/workspace/components/NameField';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { IconSettings } from '@/ui/icon';
import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer';
import { MainSectionTitle } from '@/ui/title/components/MainSectionTitle';
import { SubSectionTitle } from '@/ui/title/components/SubSectionTitle';
@ -24,7 +25,7 @@ const StyledSectionContainer = styled.div`
export function SettingsWorksapce() {
return (
<SubMenuTopBarContainer>
<SubMenuTopBarContainer icon={<IconSettings size={16} />} title="Settings">
<div>
<StyledContainer>
<MainSectionTitle>General</MainSectionTitle>

View File

@ -4,7 +4,7 @@ import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { Button } from '@/ui/button/components/Button';
import { IconTrash } from '@/ui/icon';
import { IconSettings, IconTrash } from '@/ui/icon';
import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer';
import { MainSectionTitle } from '@/ui/title/components/MainSectionTitle';
import { SubSectionTitle } from '@/ui/title/components/SubSectionTitle';
@ -75,7 +75,7 @@ export function SettingsWorkspaceMembers() {
};
return (
<SubMenuTopBarContainer>
<SubMenuTopBarContainer icon={<IconSettings size={16} />} title="Settings">
<StyledContainer>
<MainSectionTitle>Members</MainSectionTitle>
{workspace?.inviteHash && (

View File

@ -1,5 +1,5 @@
import styled from '@emotion/styled';
import { Decorator, StrictArgs } from '@storybook/react';
import { Decorator } from '@storybook/react';
function stateProps(state: string) {
switch (state) {
@ -22,11 +22,20 @@ const StyledSizeTitle = styled.h1`
margin: ${({ theme }) => theme.spacing(2)};
`;
const StyledVariantTitle = styled.h1`
const StyledVariantTitle = styled.h2`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: ${({ theme }) => theme.spacing(2)};
width: 100px;
`;
const StyledAccentTitle = styled.h3`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: ${({ theme }) => theme.spacing(2)};
width: 100px;
`;
const StyledStateTitle = styled.span`
@ -49,62 +58,58 @@ const StyledSizeContainer = styled.div`
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledLineContainer = styled.div`
const StyledVariantContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledAccentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledComponentContainer = styled.div`
const StyledStateContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
`;
function renderSize(
size: string,
variants: string[],
states: string[],
args: StrictArgs,
Story: React.FC<StrictArgs>,
) {
return (
<StyledSizeContainer key={size}>
<StyledSizeTitle>{size}</StyledSizeTitle>
{variants.map((variant) => (
<div key={variant}>
<StyledVariantTitle>{variant}</StyledVariantTitle>
<StyledLineContainer>
{states.map((state) => (
<StyledComponentContainer key={`${variant}-container-${state}`}>
<StyledStateTitle>{state}</StyledStateTitle>
<Story
args={{ ...args, variant: variant, ...stateProps(state) }}
/>
</StyledComponentContainer>
))}
</StyledLineContainer>
</div>
))}
</StyledSizeContainer>
);
}
export const ExhaustiveComponentDecorator: Decorator = (Story, context) => {
const parameters = context.parameters;
return (
<StyledContainer>
{parameters.sizes.map((size: string) =>
renderSize(
size,
parameters.variants,
parameters.states,
context.args,
Story,
),
)}
{parameters.sizes.map((size: string) => (
<StyledSizeContainer key={size}>
<StyledSizeTitle>{size}</StyledSizeTitle>
{parameters.variants.map((variant: string) => (
<StyledVariantContainer key={variant}>
<StyledVariantTitle>{variant}</StyledVariantTitle>
{parameters.accents.map((accent: string) => (
<StyledAccentContainer key={accent}>
<StyledAccentTitle>{accent}</StyledAccentTitle>
{parameters.states.map((state: string) => (
<StyledStateContainer key={state}>
<StyledStateTitle>{state}</StyledStateTitle>
<Story
args={{
...context.args,
accent: accent,
variant: variant,
...stateProps(state),
}}
/>
</StyledStateContainer>
))}
</StyledAccentContainer>
))}
</StyledVariantContainer>
))}
</StyledSizeContainer>
))}
</StyledContainer>
);
};