From 9c124981622d88a78abfada0ec19bb3268867e70 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 1 Feb 2025 09:42:51 -0500 Subject: [PATCH] Change facepile to clip path (#8325) * Change to clip path * tsc * Remove showBorder prop * fix: Facepile size prop, tons of cleanup --- app/components/Avatar/Avatar.tsx | 34 ++++--- app/components/Avatar/AvatarWithPresence.tsx | 15 ++- app/components/Avatar/Initials.tsx | 14 +-- app/components/Collaborators.tsx | 12 ++- app/components/DocumentViews.tsx | 10 +- app/components/EventListItem.tsx | 4 +- app/components/Facepile.tsx | 94 +++++++++++-------- .../Sharing/Collection/AccessControlList.tsx | 6 +- .../Sharing/Document/AccessControlList.tsx | 6 +- .../Document/DocumentMemberListItem.tsx | 4 +- .../Sharing/components/Suggestions.tsx | 8 +- app/components/Sidebar/Shared.tsx | 5 +- app/components/Sidebar/Sidebar.tsx | 1 - app/editor/components/MentionMenu.tsx | 1 - .../components/MembershipPreview.tsx | 6 +- .../Document/components/CommentThread.tsx | 5 +- app/scenes/Document/components/Insights.tsx | 4 +- app/scenes/Login/index.tsx | 5 +- app/scenes/Search/components/UserFilter.tsx | 2 +- .../Settings/components/GroupDialogs.tsx | 2 +- app/scenes/Settings/components/ImageInput.tsx | 6 +- .../Settings/components/PeopleTable.tsx | 4 +- plugins/github/client/Settings.tsx | 1 - 23 files changed, 131 insertions(+), 118 deletions(-) diff --git a/app/components/Avatar/Avatar.tsx b/app/components/Avatar/Avatar.tsx index 8e73c595f0..731e132e9c 100644 --- a/app/components/Avatar/Avatar.tsx +++ b/app/components/Avatar/Avatar.tsx @@ -7,9 +7,10 @@ export enum AvatarSize { Small = 16, Toast = 18, Medium = 24, - Large = 32, - XLarge = 48, - XXLarge = 64, + Large = 28, + XLarge = 32, + XXLarge = 48, + Upload = 64, } export interface IAvatar { @@ -20,36 +21,37 @@ export interface IAvatar { } type Props = { + /** The size of the avatar */ size: AvatarSize; + /** The source of the avatar image, if not passing a model. */ src?: string; + /** The avatar model, if not passing a source. */ model?: IAvatar; + /** The alt text for the image */ alt?: string; - showBorder?: boolean; + /** Optional click handler */ onClick?: React.MouseEventHandler; + /** Optional class name */ className?: string; + /** Optional style */ style?: React.CSSProperties; }; function Avatar(props: Props) { - const { showBorder, model, style, ...rest } = props; + const { model, style, ...rest } = props; const src = props.src || model?.avatarUrl; const [error, handleError] = useBoolean(false); return ( {src && !error ? ( - + ) : model ? ( - + {model.initial} ) : ( - + )} ); @@ -65,15 +67,11 @@ const Relative = styled.div` flex-shrink: 0; `; -const CircleImg = styled.img<{ size: number; $showBorder?: boolean }>` +const CircleImg = styled.img<{ size: number }>` display: block; width: ${(props) => props.size}px; height: ${(props) => props.size}px; border-radius: 50%; - border: ${(props) => - props.$showBorder === false - ? "none" - : `2px solid ${props.theme.background}`}; flex-shrink: 0; overflow: hidden; `; diff --git a/app/components/Avatar/AvatarWithPresence.tsx b/app/components/Avatar/AvatarWithPresence.tsx index 106c1d6ce8..c6855afe8b 100644 --- a/app/components/Avatar/AvatarWithPresence.tsx +++ b/app/components/Avatar/AvatarWithPresence.tsx @@ -5,7 +5,7 @@ import styled, { css } from "styled-components"; import { s } from "@shared/styles"; import User from "~/models/User"; import Tooltip from "~/components/Tooltip"; -import Avatar from "./Avatar"; +import Avatar, { AvatarSize } from "./Avatar"; type Props = { user: User; @@ -14,6 +14,8 @@ type Props = { isObserving: boolean; isCurrentUser: boolean; onClick?: React.MouseEventHandler; + size?: AvatarSize; + style?: React.CSSProperties; }; function AvatarWithPresence({ @@ -23,6 +25,8 @@ function AvatarWithPresence({ isEditing, isObserving, isCurrentUser, + size = AvatarSize.Large, + style, }: Props) { const { t } = useTranslation(); const status = isPresent @@ -47,13 +51,14 @@ function AvatarWithPresence({ } placement="bottom" > - - - + + ); @@ -69,7 +74,7 @@ type AvatarWrapperProps = { $color: string; }; -const AvatarWrapper = styled.div` +const AvatarPresence = styled.div` opacity: ${(props) => (props.$isPresent ? 1 : 0.5)}; transition: opacity 250ms ease-in-out; border-radius: 50%; diff --git a/app/components/Avatar/Initials.tsx b/app/components/Avatar/Initials.tsx index 296db02267..10a9e78165 100644 --- a/app/components/Avatar/Initials.tsx +++ b/app/components/Avatar/Initials.tsx @@ -3,9 +3,12 @@ import { s } from "@shared/styles"; import Flex from "~/components/Flex"; const Initials = styled(Flex)<{ + /** The color of the background, defaults to textTertiary. */ color?: string; + /** Content is only used to calculate font size, use children to render. */ + content?: string; + /** The size of the avatar */ size: number; - $showBorder?: boolean; }>` align-items: center; justify-content: center; @@ -13,15 +16,14 @@ const Initials = styled(Flex)<{ width: 100%; height: 100%; color: ${s("white75")}; - background-color: ${(props) => props.color}; + background-color: ${(props) => props.color ?? props.theme.textTertiary}; width: ${(props) => props.size}px; height: ${(props) => props.size}px; border-radius: 50%; - border: 2px solid - ${(props) => - props.$showBorder === false ? "transparent" : props.theme.background}; flex-shrink: 0; - font-size: ${(props) => props.size / 2}px; + + // adjust font size down for each additional character + font-size: ${(props) => props.size / 2 - (props.content?.length ?? 0)}px; font-weight: 500; `; diff --git a/app/components/Collaborators.tsx b/app/components/Collaborators.tsx index 9ee6e21040..e9adc999bf 100644 --- a/app/components/Collaborators.tsx +++ b/app/components/Collaborators.tsx @@ -7,7 +7,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import { usePopoverState, PopoverDisclosure } from "reakit/Popover"; import Document from "~/models/Document"; -import { AvatarWithPresence } from "~/components/Avatar"; +import { AvatarSize, AvatarWithPresence } from "~/components/Avatar"; import DocumentViews from "~/components/DocumentViews"; import Facepile from "~/components/Facepile"; import NudeButton from "~/components/NudeButton"; @@ -83,15 +83,16 @@ function Collaborators(props: Props) { {(popoverProps) => ( { + renderAvatar={({ model: collaborator, ...rest }) => { const isPresent = presentIds.includes(collaborator.id); const isEditing = editingIds.includes(collaborator.id); const isObserving = ui.observingUserId === collaborator.id; @@ -99,6 +100,7 @@ function Collaborators(props: Props) { return ( } + image={ + + } border={false} small /> diff --git a/app/components/EventListItem.tsx b/app/components/EventListItem.tsx index 83c7bf161c..4c54151e2a 100644 --- a/app/components/EventListItem.tsx +++ b/app/components/EventListItem.tsx @@ -16,7 +16,7 @@ import EventBoundary from "@shared/components/EventBoundary"; import { s, hover } from "@shared/styles"; import Document from "~/models/Document"; import Event from "~/models/Event"; -import { Avatar } from "~/components/Avatar"; +import { Avatar, AvatarSize } from "~/components/Avatar"; import Item, { Actions, Props as ItemProps } from "~/components/List/Item"; import Time from "~/components/Time"; import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext"; @@ -153,7 +153,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => { onClick={handleTimeClick} /> } - image={} + image={} subtitle={ {icon} diff --git a/app/components/Facepile.tsx b/app/components/Facepile.tsx index 914fa7e9a2..1085559872 100644 --- a/app/components/Facepile.tsx +++ b/app/components/Facepile.tsx @@ -1,17 +1,26 @@ import { observer } from "mobx-react"; import * as React from "react"; import styled from "styled-components"; -import { s } from "@shared/styles"; import User from "~/models/User"; import { Avatar, AvatarSize } from "~/components/Avatar"; import Flex from "~/components/Flex"; +import Initials from "./Avatar/Initials"; type Props = { + /** The users to display */ users: User[]; + /** The size of the avatars, defaults to AvatarSize.Large */ size?: number; + /** A number to show as the number of additional users */ overflow?: number; + /** The maximum number of users to display, defaults to 8 */ limit?: number; - renderAvatar?: (user: User) => React.ReactNode; + /** A component to render the avatar, defaults to Avatar. */ + renderAvatar?: ( + props: React.ComponentProps & { + model: User; + } + ) => React.ReactNode; }; function Facepile({ @@ -19,55 +28,64 @@ function Facepile({ overflow = 0, size = AvatarSize.Large, limit = 8, - renderAvatar = DefaultAvatar, + renderAvatar = Avatar, ...rest }: Props) { + const filtered = users.filter(Boolean).slice(-limit); + return ( {overflow > 0 && ( - - - {users.length ? "+" : ""} - {overflow} - - + + {users.length ? "+" : ""} + {overflow} + )} - {users - .filter(Boolean) - .slice(0, limit) - .map((user) => ( - {renderAvatar(user)} - ))} + {filtered.map((model, index) => { + const lastChild = index === 0 && overflow <= 0; + return renderAvatar({ + model, + size, + style: { + marginRight: lastChild ? 0 : -4, + ...(lastChild || filtered.length === 1 + ? {} + : { clipPath: `url(#${clipPathId(size)})` }), + }, + }); + })} + ); } -function DefaultAvatar(user: User) { - return ; +function FacepileClip({ size }: { size: number }) { + return ( + + + + + + ); } -const AvatarWrapper = styled.div` - margin-right: -8px; +function clipPathId(size: number) { + return `facepile-${size}`; +} - &:first-child { - margin-right: 0; - } -`; - -const More = styled.div<{ size: number }>` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-width: ${(props) => props.size}px; - height: ${(props) => props.size}px; - border-radius: 100%; - background: ${(props) => props.theme.textTertiary}; - color: ${s("white")}; - border: 2px solid ${s("background")}; - text-align: center; - font-size: 12px; - font-weight: 600; +const SVG = styled.svg` + position: absolute; + top: 0; + left: 0; `; const Avatars = styled(Flex)` diff --git a/app/components/Sharing/Collection/AccessControlList.tsx b/app/components/Sharing/Collection/AccessControlList.tsx index 1f3bea96f2..b5d20cf579 100644 --- a/app/components/Sharing/Collection/AccessControlList.tsx +++ b/app/components/Sharing/Collection/AccessControlList.tsx @@ -201,11 +201,7 @@ export const AccessControlList = observer( + } title={membership.user.name} subtitle={membership.user.email} diff --git a/app/components/Sharing/Document/AccessControlList.tsx b/app/components/Sharing/Document/AccessControlList.tsx index 63527288b0..3a3089a67a 100644 --- a/app/components/Sharing/Document/AccessControlList.tsx +++ b/app/components/Sharing/Document/AccessControlList.tsx @@ -146,7 +146,7 @@ export const AccessControlList = observer( /> ) : ( } + image={} title={user.name} subtitle={t("You have full access")} actions={{t("Can edit")}} @@ -160,9 +160,7 @@ export const AccessControlList = observer( ) : document.isDraft ? ( <> - } + image={} title={document.createdBy?.name} actions={ diff --git a/app/components/Sharing/Document/DocumentMemberListItem.tsx b/app/components/Sharing/Document/DocumentMemberListItem.tsx index 46e6c1e153..ecc89f550b 100644 --- a/app/components/Sharing/Document/DocumentMemberListItem.tsx +++ b/app/components/Sharing/Document/DocumentMemberListItem.tsx @@ -73,9 +73,7 @@ const DocumentMemberListItem = ({ return ( - } + image={} subtitle={ membership?.sourceId ? ( diff --git a/app/components/Sharing/components/Suggestions.tsx b/app/components/Sharing/components/Suggestions.tsx index 644c453add..ac2cf6d0b6 100644 --- a/app/components/Sharing/components/Suggestions.tsx +++ b/app/components/Sharing/components/Suggestions.tsx @@ -158,13 +158,7 @@ export const Suggestions = observer( : suggestion.isViewer ? t("Viewer") : t("Editor"), - image: ( - - ), + image: , }; } diff --git a/app/components/Sidebar/Shared.tsx b/app/components/Sidebar/Shared.tsx index 7ab30e00b6..41e5a6dd6b 100644 --- a/app/components/Sidebar/Shared.tsx +++ b/app/components/Sidebar/Shared.tsx @@ -14,6 +14,7 @@ import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; import history from "~/utils/history"; import { homePath, sharedDocumentPath } from "~/utils/routeHelpers"; +import { AvatarSize } from "../Avatar"; import { useTeamContext } from "../TeamContext"; import TeamLogo from "../TeamLogo"; import Sidebar from "./Sidebar"; @@ -40,7 +41,9 @@ function SharedSidebar({ rootNode, shareId }: Props) { {teamAvailable && ( } + image={ + + } onClick={() => history.push( user ? homePath() : sharedDocumentPath(shareId, rootNode.url) diff --git a/app/components/Sidebar/Sidebar.tsx b/app/components/Sidebar/Sidebar.tsx index dd7ef44910..49f6b16bd2 100644 --- a/app/components/Sidebar/Sidebar.tsx +++ b/app/components/Sidebar/Sidebar.tsx @@ -228,7 +228,6 @@ const Sidebar = React.forwardRef(function _Sidebar( alt={user.name} model={user} size={24} - showBorder={false} style={{ marginLeft: 4 }} /> } diff --git a/app/editor/components/MentionMenu.tsx b/app/editor/components/MentionMenu.tsx index badbb34ab4..dcb572900c 100644 --- a/app/editor/components/MentionMenu.tsx +++ b/app/editor/components/MentionMenu.tsx @@ -84,7 +84,6 @@ function MentionMenu({ search, isActive, ...rest }: Props) { > diff --git a/app/scenes/Collection/components/MembershipPreview.tsx b/app/scenes/Collection/components/MembershipPreview.tsx index 1c980a57ba..6811c72319 100644 --- a/app/scenes/Collection/components/MembershipPreview.tsx +++ b/app/scenes/Collection/components/MembershipPreview.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import { PAGINATION_SYMBOL } from "~/stores/base/Store"; import Collection from "~/models/Collection"; -import { Avatar, AvatarSize } from "~/components/Avatar"; +import { AvatarSize } from "~/components/Avatar"; import Facepile from "~/components/Facepile"; import Fade from "~/components/Fade"; import NudeButton from "~/components/NudeButton"; @@ -101,12 +101,10 @@ const MembershipPreview = ({ collection, limit = 8 }: Props) => { > ( - - )} /> diff --git a/app/scenes/Document/components/CommentThread.tsx b/app/scenes/Document/components/CommentThread.tsx index 737234483d..39daa0c1ae 100644 --- a/app/scenes/Document/components/CommentThread.tsx +++ b/app/scenes/Document/components/CommentThread.tsx @@ -11,7 +11,7 @@ import { ProsemirrorData } from "@shared/types"; import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper"; import Comment from "~/models/Comment"; import Document from "~/models/Document"; -import { Avatar, AvatarSize } from "~/components/Avatar"; +import { AvatarSize } from "~/components/Avatar"; import { useDocumentContext } from "~/components/DocumentContext"; import Facepile from "~/components/Facepile"; import Fade from "~/components/Fade"; @@ -149,9 +149,6 @@ function CommentThread({ limit={limit} overflow={overflow} size={AvatarSize.Medium} - renderAvatar={(item) => ( - - )} /> ); diff --git a/app/scenes/Document/components/Insights.tsx b/app/scenes/Document/components/Insights.tsx index 1ecad36799..f9ead39b3f 100644 --- a/app/scenes/Document/components/Insights.tsx +++ b/app/scenes/Document/components/Insights.tsx @@ -6,7 +6,7 @@ import styled from "styled-components"; import { s } from "@shared/styles"; import { stringToColor } from "@shared/utils/color"; import User from "~/models/User"; -import { Avatar } from "~/components/Avatar"; +import { Avatar, AvatarSize } from "~/components/Avatar"; import { useDocumentContext } from "~/components/DocumentContext"; import DocumentViews from "~/components/DocumentViews"; import Flex from "~/components/Flex"; @@ -136,7 +136,7 @@ function Insights() { avatarUrl: null, initial: document.sourceMetadata.createdByName[0], }} - size={32} + size={AvatarSize.Large} /> } subtitle={t("Creator")} diff --git a/app/scenes/Login/index.tsx b/app/scenes/Login/index.tsx index 146522e7a2..96a356abe2 100644 --- a/app/scenes/Login/index.tsx +++ b/app/scenes/Login/index.tsx @@ -10,6 +10,7 @@ import { s } from "@shared/styles"; import { UserPreference } from "@shared/types"; import { parseDomain } from "@shared/utils/domains"; import { Config } from "~/stores/AuthStore"; +import { AvatarSize } from "~/components/Avatar"; import ButtonLarge from "~/components/ButtonLarge"; import ChangeLanguage from "~/components/ChangeLanguage"; import Fade from "~/components/Fade"; @@ -249,9 +250,9 @@ function Login({ children }: Props) { /> {config.logo && !isCreate ? ( - + ) : ( - + )} {isCreate ? ( diff --git a/app/scenes/Search/components/UserFilter.tsx b/app/scenes/Search/components/UserFilter.tsx index ee5ae89ddd..7e8d7076c3 100644 --- a/app/scenes/Search/components/UserFilter.tsx +++ b/app/scenes/Search/components/UserFilter.tsx @@ -25,7 +25,7 @@ function UserFilter(props: Props) { const userOptions = users.all.map((user) => ({ key: user.id, label: user.name, - icon: , + icon: , })); return [ { diff --git a/app/scenes/Settings/components/GroupDialogs.tsx b/app/scenes/Settings/components/GroupDialogs.tsx index 0f1c5e4792..6941cf78e8 100644 --- a/app/scenes/Settings/components/GroupDialogs.tsx +++ b/app/scenes/Settings/components/GroupDialogs.tsx @@ -434,7 +434,7 @@ const GroupMemberListItem = observer(function ({ {user.isAdmin && {t("Admin")}} } - image={} + image={} actions={ {onRemove && } diff --git a/app/scenes/Settings/components/ImageInput.tsx b/app/scenes/Settings/components/ImageInput.tsx index 9d795cad0d..267c4b1e6f 100644 --- a/app/scenes/Settings/components/ImageInput.tsx +++ b/app/scenes/Settings/components/ImageInput.tsx @@ -18,7 +18,7 @@ export default function ImageInput({ model, onSuccess, ...rest }: Props) { - + {t("Upload")} @@ -34,8 +34,8 @@ export default function ImageInput({ model, onSuccess, ...rest }: Props) { } const avatarStyles = ` - width: ${AvatarSize.XXLarge}px; - height: ${AvatarSize.XXLarge}px; + width: ${AvatarSize.Upload}px; + height: ${AvatarSize.Upload}px; `; const StyledAvatar = styled(Avatar)` diff --git a/app/scenes/Settings/components/PeopleTable.tsx b/app/scenes/Settings/components/PeopleTable.tsx index f2d4419982..9e31b7b835 100644 --- a/app/scenes/Settings/components/PeopleTable.tsx +++ b/app/scenes/Settings/components/PeopleTable.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import User from "~/models/User"; -import { Avatar } from "~/components/Avatar"; +import { Avatar, AvatarSize } from "~/components/Avatar"; import Badge from "~/components/Badge"; import Flex from "~/components/Flex"; import { HEADER_HEIGHT } from "~/components/Header"; @@ -38,7 +38,7 @@ export function PeopleTable({ canManage, ...rest }: Props) { accessor: (user) => user.name, component: (user) => ( - {user.name}{" "} + {user.name}{" "} {currentUser.id === user.id && `(${t("You")})`} ), diff --git a/plugins/github/client/Settings.tsx b/plugins/github/client/Settings.tsx index f42c25c837..c1cd713367 100644 --- a/plugins/github/client/Settings.tsx +++ b/plugins/github/client/Settings.tsx @@ -116,7 +116,6 @@ function GitHub() { } actions={