UBER-486: updated people avatars. (#3720)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2023-09-20 20:01:09 +03:00 committed by GitHub
parent d9d47846cf
commit a07f88033f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 322 additions and 111 deletions

View File

@ -947,6 +947,7 @@ a.no-line {
.content-color { color: var(--theme-content-color); } .content-color { color: var(--theme-content-color); }
.caption-color { color: var(--theme-caption-color); } .caption-color { color: var(--theme-caption-color); }
.content-accented-color { color: var(--accented-button-color); }
.red-color { color: var(--highlight-red); } .red-color { color: var(--highlight-red); }
.error-color { color: var(--theme-error-color); } .error-color { color: var(--theme-error-color); }

View File

@ -194,7 +194,9 @@
&.accented, &.brand, &.positive, &.negative { &.accented, &.brand, &.positive, &.negative {
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
color: var(--accented-button-color); color: var(--accented-button-color);
.btn-icon { color: var(--accented-button-color); }
.btn-icon,
.btn-right-icon { color: var(--accented-button-color); }
} }
} }
&.regular, &.ghost { &.regular, &.ghost {
@ -208,7 +210,8 @@
color: var(--accented-button-content-color); color: var(--accented-button-content-color);
border-color: var(--accented-button-border); border-color: var(--accented-button-border);
.btn-icon { color: var(--accented-button-content-color); } .btn-icon,
.btn-right-icon { color: var(--accented-button-content-color); }
} }
&.accented { &.accented {
background-color: var(--accented-button-default); background-color: var(--accented-button-default);
@ -257,7 +260,8 @@
background-color: var(--theme-button-contrast-enabled); background-color: var(--theme-button-contrast-enabled);
border-color: var(--theme-button-contrast-border); border-color: var(--theme-button-contrast-border);
.btn-icon { color: var(--theme-button-contrast-color); } .btn-icon,
.btn-right-icon { color: var(--theme-button-contrast-color); }
&:hover { background-color: var(--theme-button-contrast-hovered); } &:hover { background-color: var(--theme-button-contrast-hovered); }
&:active { background-color: var(--theme-button-contrast-pressed); } &:active { background-color: var(--theme-button-contrast-pressed); }
@ -298,7 +302,8 @@
border-color: transparent; border-color: transparent;
cursor: not-allowed; cursor: not-allowed;
.btn-icon { opacity: .5; } .btn-icon,
.btn-right-icon { opacity: .5; }
} }
.resetIconSize { font-size: 16px; } .resetIconSize { font-size: 16px; }

View File

@ -93,7 +93,10 @@
/> />
</span> </span>
<svelte:fragment slot="iconRight"> <svelte:fragment slot="iconRight">
<DropdownIcon size={'small'} fill={'var(--theme-dark-color)'} /> <DropdownIcon
size={'small'}
fill={kind === 'accented' && !disabled ? 'var(--accented-button-content-color)' : 'var(--theme-dark-color)'}
/>
</svelte:fragment> </svelte:fragment>
</Button> </Button>
</div> </div>

View File

@ -191,7 +191,10 @@
{#if showIcon} {#if showIcon}
{#if withAvatar} {#if withAvatar}
<div class="msgactivity-avatar"> <div class="msgactivity-avatar">
<Component is={contact.component.Avatar} props={{ avatar: person?.avatar, size: 'medium' }} /> <Component
is={contact.component.Avatar}
props={{ avatar: person?.avatar, size: 'medium', name: person?.name }}
/>
</div> </div>
{:else} {:else}
<div class="msgactivity-icon"> <div class="msgactivity-icon">

View File

@ -45,7 +45,7 @@
on:click={() => onClick(p)} on:click={() => onClick(p)}
> >
<div class="icon"> <div class="icon">
<Avatar size={'x-small'} avatar={p.avatar} /> <Avatar size={'x-small'} avatar={p.avatar} name={p.name} />
</div> </div>
</div> </div>
{/each} {/each}

View File

@ -28,7 +28,7 @@
</script> </script>
<div class="flex-nowrap"> <div class="flex-nowrap">
<div class="avatar"><Avatar size={'medium'} /></div> <div class="avatar"><Avatar size={'medium'} avatar={user.avatar} name={user.name} /></div>
<div class="flex-col-stretch message"> <div class="flex-col-stretch message">
<div class="header">{getName(client.getHierarchy(), user)}<span>{message.createDate}</span></div> <div class="header">{getName(client.getHierarchy(), user)}<span>{message.createDate}</span></div>
<div class="text">{message.text}</div> <div class="text">{message.text}</div>

View File

@ -74,7 +74,7 @@
<div class="flex-row-top"> <div class="flex-row-top">
{#await getEmployee(value, $personByIdStore, $personAccountByIdStore) then employee} {#await getEmployee(value, $personByIdStore, $personAccountByIdStore) then employee}
<div class="avatar"> <div class="avatar">
<Avatar size={'medium'} avatar={employee?.avatar} /> <Avatar size={'medium'} avatar={employee?.avatar} name={employee?.name} />
</div> </div>
<div class="flex-grow flex-col select-text"> <div class="flex-grow flex-col select-text">
<div class="header"> <div class="header">

View File

@ -204,7 +204,9 @@
</script> </script>
<div class="container clear-mins" class:highlighted={isHighlighted} id={message._id}> <div class="container clear-mins" class:highlighted={isHighlighted} id={message._id}>
<div class="avatar"><Avatar size={'medium'} avatar={employee?.avatar} /></div> <div class="avatar">
<Avatar size={'medium'} avatar={employee?.avatar} name={employee?.name} />
</div>
<div class="message clear-mins"> <div class="message clear-mins">
<div class="header clear-mins"> <div class="header clear-mins">
{#if employee} {#if employee}

View File

@ -52,7 +52,7 @@
<div class="message"> <div class="message">
<div class="header"> <div class="header">
<div class="avatar"> <div class="avatar">
<Avatar size={'medium'} avatar={employee?.avatar} /> <Avatar size={'medium'} avatar={employee?.avatar} name={employee?.name} />
</div> </div>
<span class="name"> <span class="name">
{employee ? getName(client.getHierarchy(), employee) : ''} {employee ? getName(client.getHierarchy(), employee) : ''}

View File

@ -59,7 +59,7 @@
<div class="flex-row-center container cursor-pointer" on:click> <div class="flex-row-center container cursor-pointer" on:click>
<div class="flex-row-center"> <div class="flex-row-center">
{#each showReplies as reply} {#each showReplies as reply}
<div class="reply"><Avatar size={'x-small'} avatar={reply.avatar} /></div> <div class="reply"><Avatar size={'x-small'} avatar={reply.avatar} name={reply.name} /></div>
{/each} {/each}
{#if employees.size > shown} {#if employees.size > shown}
<div class="reply"><span>+{employees.size - shown}</span></div> <div class="reply"><span>+{employees.size - shown}</span></div>

View File

@ -27,7 +27,13 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import contact, { AvatarProvider, AvatarType } from '@hcengineering/contact' import contact, {
AvatarProvider,
AvatarType,
getAvatarColorForId,
getFirstName,
getLastName
} from '@hcengineering/contact'
import { Client, Ref } from '@hcengineering/core' import { Client, Ref } from '@hcengineering/core'
import { Asset, getResource } from '@hcengineering/platform' import { Asset, getResource } from '@hcengineering/platform'
import { getBlobURL, getClient } from '@hcengineering/presentation' import { getBlobURL, getClient } from '@hcengineering/presentation'
@ -36,14 +42,21 @@
import AvatarIcon from './icons/Avatar.svelte' import AvatarIcon from './icons/Avatar.svelte'
export let avatar: string | null | undefined = undefined export let avatar: string | null | undefined = undefined
export let name: string | null | undefined = undefined
export let direct: Blob | undefined = undefined export let direct: Blob | undefined = undefined
export let size: IconSize export let size: IconSize
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
let url: string[] | undefined let url: string[] | undefined
let avatarProvider: AvatarProvider | undefined let avatarProvider: AvatarProvider | undefined
let color: string | undefined = undefined
async function update (size: IconSize, avatar?: string | null, direct?: Blob) { $: fname = getFirstName(name ?? '')
$: lname = getLastName(name ?? '')
$: displayName =
name != null ? (lname.length > 1 ? lname.trim()[0] : lname) + (fname.length > 1 ? fname.trim()[0] : fname) : ''
async function update (size: IconSize, avatar?: string | null, direct?: Blob, name?: string | null) {
if (direct !== undefined) { if (direct !== undefined) {
getBlobURL(direct).then((blobURL) => { getBlobURL(direct).then((blobURL) => {
url = [blobURL] url = [blobURL]
@ -55,32 +68,46 @@
if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) { if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) {
url = undefined url = undefined
color = avatar.split('://')[1]
} else if (avatarProvider?.type === AvatarType.IMAGE) { } else if (avatarProvider?.type === AvatarType.IMAGE) {
url = (await getResource(avatarProvider.getUrl))(avatar, size) url = (await getResource(avatarProvider.getUrl))(avatar, size)
} else { } else {
const uri = avatar.split('://')[1] const uri = avatar.split('://')[1]
url = (await getResource(avatarProvider.getUrl))(uri, size) url = (await getResource(avatarProvider.getUrl))(uri, size)
} }
} else if (name != null) {
color = getAvatarColorForId(name)
url = undefined
avatarProvider = undefined
} else { } else {
url = undefined url = undefined
avatarProvider = undefined avatarProvider = undefined
} }
} }
$: update(size, avatar, direct) $: update(size, avatar, direct, name)
let imageElement: HTMLImageElement | undefined = undefined let imageElement: HTMLImageElement | undefined = undefined
$: srcset = url?.slice(1)?.join(', ') $: srcset = url?.slice(1)?.join(', ')
</script> </script>
<div class="ava-{size} flex-center avatar-container" class:no-img={!url}> <div
class="ava-{size} flex-center avatar-container"
class:no-img={!url && color}
class:bordered={!url && color === undefined}
style:background-color={url ? 'var(--theme-button-default)' : color}
>
{#if url} {#if url}
{#if size === 'large' || size === 'x-large' || size === '2x-large'} {#if size === 'large' || size === 'x-large' || size === '2x-large'}
<img class="ava-{size} ava-blur" src={url[0]} {srcset} alt={''} bind:this={imageElement} /> <img class="ava-{size} ava-blur" src={url[0]} {srcset} alt={''} bind:this={imageElement} />
{/if} {/if}
<img class="ava-{size} ava-mask" src={url[0]} {srcset} alt={''} bind:this={imageElement} /> <img class="ava-{size} ava-mask" src={url[0]} {srcset} alt={''} bind:this={imageElement} />
{:else if name && displayName && displayName !== ''}
<div class="ava-text" data-name={displayName.toLocaleUpperCase()} />
{:else} {:else}
<Icon icon={icon ?? AvatarIcon} size={size === 'card' ? 'x-small' : size} /> <div class="icon">
<Icon icon={icon ?? AvatarIcon} size={'full'} />
</div>
{/if} {/if}
</div> </div>
@ -89,60 +116,139 @@
flex-shrink: 0; flex-shrink: 0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background-color: var(--avatar-bg-color); background-color: var(--theme-button-default);
border-radius: 50%; border-radius: 50%;
pointer-events: none; pointer-events: none;
&.no-img {
color: var(--accented-button-color);
border-color: transparent;
}
&.bordered {
color: var(--theme-dark-color);
border: 1px solid var(--theme-button-border);
}
img { img {
object-fit: cover; object-fit: cover;
border: 1px solid var(--avatar-border-color); border: 1px solid var(--avatar-border-color);
} }
&.no-img { .icon,
border-color: transparent; .ava-text::after {
position: absolute;
top: 50%;
left: 50%;
}
.icon {
width: 100%;
height: 100%;
color: inherit;
transform-origin: center;
transform: translate(-50%, -50%) scale(0.6);
}
.ava-text::after {
content: attr(data-name);
transform: translate(-50%, -50%);
} }
} }
.ava-inline { .ava-inline {
width: 0.875rem; // 24 width: 0.875rem; // 24
height: 0.875rem; height: 0.875rem;
.ava-text {
font-weight: 500;
font-size: 0.525rem;
letter-spacing: -0.05em;
}
} }
.ava-tiny { .ava-tiny {
width: 1.13rem; // ~18 width: 1.13rem; // ~18
height: 1.13rem; height: 1.13rem;
.ava-text {
font-weight: 500;
font-size: 0.625rem;
letter-spacing: -0.05em;
}
} }
.ava-card { .ava-card {
width: 1.25rem; // 20 width: 1.25rem; // 20
height: 1.25rem; height: 1.25rem;
.ava-text {
font-weight: 500;
font-size: 0.625rem;
letter-spacing: -0.05em;
}
} }
.ava-x-small { .ava-x-small {
width: 1.5rem; // 24 width: 1.5rem; // 24
height: 1.5rem; height: 1.5rem;
.ava-text {
font-weight: 500;
font-size: 0.75rem;
letter-spacing: -0.05em;
}
} }
.ava-smaller { .ava-smaller {
width: 1.75rem; // 28 width: 1.75rem; // 28
height: 1.75rem; height: 1.75rem;
.ava-text {
font-weight: 500;
font-size: 0.8125rem;
letter-spacing: -0.05em;
}
} }
.ava-small { .ava-small {
width: 2rem; // 32 width: 2rem; // 32
height: 2rem; height: 2rem;
.ava-text {
font-weight: 500;
font-size: 0.875rem;
letter-spacing: -0.05em;
}
} }
.ava-medium { .ava-medium {
width: 2.25rem; // 36 width: 2.25rem; // 36
height: 2.25rem; height: 2.25rem;
.ava-text {
font-weight: 500;
font-size: 0.875rem;
letter-spacing: -0.05em;
}
} }
.ava-large { .ava-large {
width: 4.5rem; // 72 width: 4.5rem; // 72
height: 4.5rem; height: 4.5rem;
.ava-text {
font-weight: 500;
font-size: 2rem;
}
} }
.ava-x-large { .ava-x-large {
width: 7.5rem; // 120 width: 7.5rem; // 120
height: 7.5rem; height: 7.5rem;
.ava-text {
font-weight: 500;
font-size: 3.5rem;
}
} }
.ava-2x-large { .ava-2x-large {
width: 10rem; // 120 width: 10rem; // 120
height: 10rem; height: 10rem;
.ava-text {
font-weight: 500;
font-size: 4.75rem;
}
} }
.ava-blur { .ava-blur {

View File

@ -56,7 +56,7 @@
{/if} {/if}
{#each persons as person, i} {#each persons as person, i}
<div class="combine-avatar {size}" data-over={getDataOver(persons.length === i + 1, items)}> <div class="combine-avatar {size}" data-over={getDataOver(persons.length === i + 1, items)}>
<Avatar avatar={person.avatar} {size} /> <Avatar avatar={person.avatar} {size} name={person.name} />
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -169,7 +169,13 @@
<slot name="extraControls" /> <slot name="extraControls" />
</div> </div>
<div class="ml-4"> <div class="ml-4">
<EditableAvatar avatar={object.avatar} {email} {id} size={'large'} bind:this={avatarEditor} /> <EditableAvatar
avatar={object.avatar}
name={combineName(firstName, lastName)}
{email}
size={'large'}
bind:this={avatarEditor}
/>
</div> </div>
</div> </div>
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">

View File

@ -115,7 +115,12 @@
</div> </div>
</div> </div>
<div class="ml-4"> <div class="ml-4">
<EditableAvatar avatar={object.avatar} {id} size={'large'} bind:this={avatarEditor} /> <EditableAvatar
avatar={object.avatar}
name={combineName(firstName, lastName)}
size={'large'}
bind:this={avatarEditor}
/>
</div> </div>
</div> </div>
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">

View File

@ -110,13 +110,13 @@
<EditableAvatar <EditableAvatar
avatar={object.avatar} avatar={object.avatar}
{email} {email}
id={object._id}
size={'x-large'} size={'x-large'}
name={object.name}
bind:this={avatarEditor} bind:this={avatarEditor}
on:done={onAvatarDone} on:done={onAvatarDone}
/> />
{:else} {:else}
<Avatar avatar={object.avatar} size={'x-large'} /> <Avatar avatar={object.avatar} size={'x-large'} name={object.name} />
{/if} {/if}
{/key} {/key}
</div> </div>

View File

@ -85,8 +85,8 @@
{#key object} {#key object}
<EditableAvatar <EditableAvatar
avatar={object.avatar} avatar={object.avatar}
id={object._id}
size={'x-large'} size={'x-large'}
name={object.name}
bind:this={avatarEditor} bind:this={avatarEditor}
on:done={onAvatarDone} on:done={onAvatarDone}
/> />
@ -131,7 +131,7 @@
.name { .name {
font-weight: 500; font-weight: 500;
font-size: 1.25rem; font-size: 1.25rem;
color: var(--caption-color); color: var(--theme-caption-color);
} }
.location { .location {
margin-top: 0.25rem; margin-top: 0.25rem;
@ -141,6 +141,6 @@
.separator { .separator {
margin: 1rem 0; margin: 1rem 0;
height: 1px; height: 1px;
background-color: var(--divider-color); background-color: var(--theme-divider-color);
} }
</style> </style>

View File

@ -16,24 +16,33 @@
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import { AnySvelteComponent, IconSize, showPopup } from '@hcengineering/ui' import { AnySvelteComponent, IconSize, showPopup } from '@hcengineering/ui'
import { AvatarType } from '@hcengineering/contact' import { AvatarType, getAvatarColorForId } from '@hcengineering/contact'
import { Asset, getResource } from '@hcengineering/platform' import { Asset, getResource } from '@hcengineering/platform'
import AvatarComponent from './Avatar.svelte' import AvatarComponent from './Avatar.svelte'
import SelectAvatarPopup from './SelectAvatarPopup.svelte' import SelectAvatarPopup from './SelectAvatarPopup.svelte'
export let avatar: string | null | undefined export let avatar: string | null | undefined
export let name: string | null | undefined = undefined
export let email: string | undefined = undefined export let email: string | undefined = undefined
export let id: string
export let size: IconSize export let size: IconSize
export let direct: Blob | undefined = undefined export let direct: Blob | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
export let disabled: boolean = false export let disabled: boolean = false
const [schema, uri] = avatar?.split('://') || [] $: [schema, uri] = avatar?.split('://') || []
let selectedAvatarType: AvatarType | undefined = avatar?.includes('://') ? (schema as AvatarType) : AvatarType.IMAGE let selectedAvatarType: AvatarType | undefined
let selectedAvatar: string | null | undefined = selectedAvatarType === AvatarType.IMAGE ? avatar : uri let selectedAvatar: string | null | undefined
$: selectedAvatarType = avatar?.includes('://')
? (schema as AvatarType)
: avatar === undefined
? AvatarType.COLOR
: AvatarType.IMAGE
$: selectedAvatar = selectedAvatarType === AvatarType.IMAGE ? avatar : uri
$: if (selectedAvatar === undefined && selectedAvatarType === AvatarType.COLOR) {
selectedAvatar = getAvatarColorForId(name)
}
export async function createAvatar (): Promise<string | undefined> { export async function createAvatar (): Promise<string | undefined> {
if (selectedAvatarType === AvatarType.IMAGE && direct !== undefined) { if (selectedAvatarType === AvatarType.IMAGE && direct !== undefined) {
@ -58,13 +67,26 @@
selectedAvatarType = submittedAvatarType selectedAvatarType = submittedAvatarType
selectedAvatar = submittedAvatar selectedAvatar = submittedAvatar
direct = submittedDirect direct = submittedDirect
avatar = selectedAvatarType === AvatarType.IMAGE ? selectedAvatar : `${selectedAvatarType}://${selectedAvatar}`
dispatch('done') dispatch('done')
} }
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
async function showSelectionPopup (e: MouseEvent) { async function showSelectionPopup (e: MouseEvent) {
if (!disabled) { if (!disabled) {
showPopup(SelectAvatarPopup, { avatar, email, id, file: direct, icon, onSubmit: handlePopupSubmit }) showPopup(SelectAvatarPopup, {
avatar:
selectedAvatarType === AvatarType.IMAGE
? selectedAvatar
: selectedAvatarType === AvatarType.COLOR && avatar == null
? undefined
: `${selectedAvatarType}://${selectedAvatar}`,
email,
name,
file: direct,
icon,
onSubmit: handlePopupSubmit
})
} }
} }
</script> </script>
@ -72,9 +94,14 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="cursor-pointer" on:click|self={showSelectionPopup}> <div class="cursor-pointer" on:click|self={showSelectionPopup}>
<AvatarComponent <AvatarComponent
avatar={selectedAvatarType === AvatarType.IMAGE ? selectedAvatar : `${selectedAvatarType}://${selectedAvatar}`}
{direct} {direct}
{size} {size}
{icon} {icon}
avatar={selectedAvatarType === AvatarType.IMAGE
? selectedAvatar
: selectedAvatarType === AvatarType.COLOR && avatar == null
? undefined
: `${selectedAvatarType}://${selectedAvatar}`}
{name}
/> />
</div> </div>

View File

@ -58,7 +58,7 @@
> >
{#if employee} {#if employee}
<div class="flex-col-center pb-2"> <div class="flex-col-center pb-2">
<Avatar size="x-large" avatar={employee.avatar} /> <Avatar size={'x-large'} avatar={employee.avatar} name={employee.name} />
</div> </div>
<div class="pb-2">{getName(client.getHierarchy(), employee)}</div> <div class="pb-2">{getName(client.getHierarchy(), employee)}</div>
<DocNavLink object={employee}> <DocNavLink object={employee}>
@ -79,6 +79,7 @@
</div> </div>
</div> </div>
{:else if editable} {:else if editable}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-row-stretch over-underline pb-2" on:click={onEdit}> <div class="flex-row-stretch over-underline pb-2" on:click={onEdit}>
<Label label={contact.string.SetStatus} /> <Label label={contact.string.SetStatus} />
</div> </div>

View File

@ -359,7 +359,7 @@
selected={update.avatar !== undefined} selected={update.avatar !== undefined}
> >
<svelte:fragment slot="item" let:item> <svelte:fragment slot="item" let:item>
<Avatar avatar={item.avatar} size={'x-large'} icon={contact.icon.Person} /> <Avatar avatar={item.avatar} size={'x-large'} icon={contact.icon.Person} name={item.name} />
</svelte:fragment> </svelte:fragment>
</MergeComparer> </MergeComparer>
<MergeComparer <MergeComparer

View File

@ -42,7 +42,7 @@
<div class="antiContactCard"> <div class="antiContactCard">
<div class="label uppercase"><Label label={contact.string.Person} /></div> <div class="label uppercase"><Label label={contact.string.Person} /></div>
<div class="flex-center logo"> <div class="flex-center logo">
<Avatar avatar={object.avatar} size={'large'} icon={contact.icon.Company} /> <Avatar avatar={object.avatar} size={'large'} icon={contact.icon.Company} name={object.name} />
</div> </div>
{#if object} {#if object}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->

View File

@ -87,7 +87,7 @@
class:mr-2={shouldShowName && !enlargedText} class:mr-2={shouldShowName && !enlargedText}
class:mr-3={shouldShowName && enlargedText} class:mr-3={shouldShowName && enlargedText}
> >
<Avatar size={avatarSize} avatar={value.avatar} /> <Avatar size={avatarSize} avatar={value.avatar} name={value.name} />
</span> </span>
{/if} {/if}
{#if shouldShowName} {#if shouldShowName}

View File

@ -15,24 +15,33 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { AvatarType, buildGravatarId, checkHasGravatar, getAvatarColorForId } from '@hcengineering/contact' import {
AvatarType,
buildGravatarId,
checkHasGravatar,
getAvatarColorForId,
getAvatarColors,
getAvatarColorName
} from '@hcengineering/contact'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
import { AnySvelteComponent, Label, showPopup, TabList } from '@hcengineering/ui' import { AnySvelteComponent, Label, showPopup, TabList, eventToHTMLElement } from '@hcengineering/ui'
import { ColorsPopup } from '@hcengineering/view-resources'
import presentation, { Card, getFileUrl } from '@hcengineering/presentation' import presentation, { Card, getFileUrl } from '@hcengineering/presentation'
import contact from '../plugin' import contact from '../plugin'
import { getAvatarTypeDropdownItems } from '../utils' import { getAvatarTypeDropdownItems } from '../utils'
import AvatarComponent from './Avatar.svelte' import AvatarComponent from './Avatar.svelte'
import EditAvatarPopup from './EditAvatarPopup.svelte' import EditAvatarPopup from './EditAvatarPopup.svelte'
export let avatar: string | undefined export let avatar: string | null | undefined = undefined
export let name: string | null | undefined = undefined
export let email: string | undefined export let email: string | undefined
export let id: string
export let file: Blob | undefined export let file: Blob | undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
export let onSubmit: (avatarType?: AvatarType, avatar?: string, file?: Blob) => void export let onSubmit: (avatarType?: AvatarType, avatar?: string, file?: Blob) => void
const [schema, uri] = avatar?.split('://') || [] const [schema, uri] = avatar?.split('://') || []
const colors = getAvatarColors()
let color: string | undefined = (schema as AvatarType) === AvatarType.COLOR ? uri : undefined
const initialSelectedType = (() => { const initialSelectedType = (() => {
if (file) { if (file) {
@ -47,7 +56,7 @@
const initialSelectedAvatar = (() => { const initialSelectedAvatar = (() => {
if (!avatar) { if (!avatar) {
return getAvatarColorForId(id) return getAvatarColorForId(name)
} }
return avatar.includes('://') ? uri : avatar return avatar.includes('://') ? uri : avatar
@ -87,7 +96,7 @@
inputRef.click() inputRef.click()
} }
} else { } else {
selectedAvatar = getAvatarColorForId(id) selectedAvatar = color ?? getAvatarColorForId(name)
} }
} }
@ -110,13 +119,13 @@
if (blob === undefined) { if (blob === undefined) {
if (!selectedFile && (!avatar || avatar.includes('://'))) { if (!selectedFile && (!avatar || avatar.includes('://'))) {
selectedAvatarType = AvatarType.COLOR selectedAvatarType = AvatarType.COLOR
selectedAvatar = getAvatarColorForId(id) selectedAvatar = getAvatarColorForId(name)
} }
return return
} }
if (blob === null) { if (blob === null) {
selectedAvatarType = AvatarType.COLOR selectedAvatarType = AvatarType.COLOR
selectedAvatar = getAvatarColorForId(id) selectedAvatar = getAvatarColorForId(name)
selectedFile = undefined selectedFile = undefined
} else { } else {
selectedFile = blob selectedFile = blob
@ -142,10 +151,23 @@
if (!inputRef.value.length) { if (!inputRef.value.length) {
if (!selectedFile) { if (!selectedFile) {
selectedAvatarType = AvatarType.COLOR selectedAvatarType = AvatarType.COLOR
selectedAvatar = getAvatarColorForId(id) selectedAvatar = getAvatarColorForId(name)
} }
} }
} }
const showColorPopup = (event: MouseEvent) => {
showPopup(
ColorsPopup,
{ colors, columns: 6, selected: getAvatarColorName(selectedAvatar) },
eventToHTMLElement(event),
(col) => {
if (col != null) {
color = selectedAvatar = colors[col].color
}
}
)
}
</script> </script>
<Card <Card
@ -164,14 +186,26 @@
on:changeContent on:changeContent
> >
<div class="flex-col-center gapV-4 mx-6"> <div class="flex-col-center gapV-4 mx-6">
{#if selectedAvatarType === AvatarType.IMAGE}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="cursor-pointer" on:click|self={handleImageAvatarClick}> <div
<AvatarComponent avatar={selectedAvatar} direct={selectedFile} size={'2x-large'} {icon} /> class="cursor-pointer"
on:click|self={(e) => {
if (selectedAvatarType === AvatarType.IMAGE) handleImageAvatarClick()
else if (selectedAvatarType === AvatarType.COLOR) showColorPopup(e)
}}
>
<AvatarComponent
avatar={selectedAvatarType === AvatarType.IMAGE
? selectedAvatar === ''
? `${AvatarType.COLOR}://${color}`
: selectedAvatar
: `${selectedAvatarType}://${selectedAvatar}`}
direct={selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined}
size={'2x-large'}
{icon}
{name}
/>
</div> </div>
{:else}
<AvatarComponent avatar={`${selectedAvatarType}://${selectedAvatar}`} size={'2x-large'} {icon} />
{/if}
<TabList <TabList
items={getAvatarTypeDropdownItems(hasGravatar)} items={getAvatarTypeDropdownItems(hasGravatar)}
kind={'separated-free'} kind={'separated-free'}

View File

@ -31,7 +31,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-row-center" on:click> <div class="flex-row-center" on:click>
<Avatar avatar={value.avatar} {size} {icon} on:accent-color /> <Avatar avatar={value.avatar} {size} {icon} name={value.name} on:accent-color />
<div class="flex-col min-w-0 {size === 'tiny' || size === 'inline' ? 'ml-1' : 'ml-2'}" class:max-w-20={short}> <div class="flex-col min-w-0 {size === 'tiny' || size === 'inline' ? 'ml-1' : 'ml-2'}" class:max-w-20={short}>
{#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if} {#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if}
<div class="label overflow-label text-left">{getName(client.getHierarchy(), value)}</div> <div class="label overflow-label text-left">{getName(client.getHierarchy(), value)}</div>

View File

@ -18,33 +18,29 @@
export let size: IconSize export let size: IconSize
export let fill: string = 'var(--caption-color)' export let fill: string = 'var(--theme-caption-color)'
</script> </script>
<svg class="svg-avatar avaicon-{size}" {fill} viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"> <svg class="svg-avatar avaicon-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<circle class="op" cx="20" cy="13.6" r="6.4" />
<path <path
d="M33.1,33.3c-0.8-2.2-2.5-4.2-4.9-5.5c-2.3-1.3-5.2-2.1-8.2-2.1s-5.8,0.7-8.2,2.1c-2.4,1.4-4.1,3.3-4.9,5.5 c-0.1,0.4,0.1,0.8,0.5,1c0.4,0.1,0.8-0.1,1-0.5c0.7-1.9,2.2-3.5,4.2-4.7c2.1-1.2,4.7-1.9,7.4-1.9c2.7,0,5.3,0.7,7.4,1.9 c2.1,1.2,3.6,2.9,4.2,4.7c0.1,0.3,0.4,0.5,0.7,0.5c0.1,0,0.2,0,0.3,0C33,34.1,33.2,33.7,33.1,33.3z" fill-rule="evenodd"
clip-rule="evenodd"
d="M10 9.99988C12.0711 9.99988 13.75 8.32095 13.75 6.24988C13.75 4.17881 12.0711 2.49988 10 2.49988C7.92893 2.49988 6.25 4.17881 6.25 6.24988C6.25 8.32095 7.92893 9.99988 10 9.99988ZM10 11.2499C12.7614 11.2499 15 9.0113 15 6.24988C15 3.48845 12.7614 1.24988 10 1.24988C7.23858 1.24988 5 3.48845 5 6.24988C5 9.0113 7.23858 11.2499 10 11.2499Z"
/> />
<path <path
d="M20,20.8c3.9,0,7.1-3.2,7.1-7.1S23.9,6.5,20,6.5c-3.9,0-7.1,3.2-7.1,7.1S16.1,20.8,20,20.8z M20,8 c3.1,0,5.6,2.5,5.6,5.6s-2.5,5.6-5.6,5.6c-3.1,0-5.6-2.5-5.6-5.6S16.9,8,20,8z" d="M8.125 12.4999C5.70875 12.4999 3.75 14.4586 3.75 16.8749V18.1249C3.75 18.4701 4.02982 18.7499 4.375 18.7499C4.72018 18.7499 5 18.4701 5 18.1249V16.8749C5 15.149 6.39911 13.7499 8.125 13.7499H11.875C13.6009 13.7499 15 15.149 15 16.8749V18.1249C15 18.4701 15.2798 18.7499 15.625 18.7499C15.9702 18.7499 16.25 18.4701 16.25 18.1249V16.8749C16.25 14.4586 14.2912 12.4999 11.875 12.4999H8.125Z"
/> />
</svg> </svg>
<style lang="scss"> <style lang="scss">
.svg-avatar {
.op {
opacity: 0.05;
}
}
.avaicon-inline { .avaicon-inline {
width: 0.75rem; width: 0.6125rem;
height: 0.75rem; height: 0.6125rem;
} }
.avaicon-tiny { .avaicon-tiny {
width: 0.875rem; width: 0.8125rem;
height: 0.875rem; height: 0.8125rem;
} }
.avaicon-x-small { .avaicon-x-small {
@ -64,15 +60,15 @@
height: 1.5rem; height: 1.5rem;
} }
.avaicon-large { .avaicon-large {
width: 1.75rem; width: 2.5rem;
height: 1.75rem; height: 2.5rem;
} }
.avaicon-x-large { .avaicon-x-large {
width: 3rem;
height: 3rem;
}
.avaicon-2x-large {
width: 4rem; width: 4rem;
height: 4rem; height: 4rem;
} }
.avaicon-2x-large {
width: 6rem;
height: 6rem;
}
</style> </style>

View File

@ -13,6 +13,8 @@
// limitations under the License. // limitations under the License.
// //
import { ColorDefinition } from '@hcengineering/ui'
/** /**
* @public * @public
*/ */
@ -29,17 +31,17 @@ export type GravatarPlaceholderType =
/** /**
* @public * @public
*/ */
export const AVATAR_COLORS = [ export const AVATAR_COLORS: ColorDefinition[] = [
'#4674ca', // blue { name: 'blue', color: '#4674ca' }, // blue
'#315cac', // blue_dark { name: 'blue_dark', color: '#315cac' }, // blue_dark
'#57be8c', // green { name: 'green', color: '#57be8c' }, // green
'#3fa372', // green_dark { name: 'green_dark', color: '#3fa372' }, // green_dark
'#f9a66d', // yellow_orange { name: 'yellow_orange', color: '#f9a66d' }, // yellow_orange
'#ec5e44', // red { name: 'red', color: '#ec5e44' }, // red
'#e63717', // red_dark { name: 'red_dark', color: '#e63717' }, // red_dark
'#f868bc', // pink { name: 'pink', color: '#f868bc' }, // pink
'#6c5fc7', // purple { name: 'purple', color: '#6c5fc7' }, // purple
'#4e3fb4', // purple_dark { name: 'purple_dark', color: '#4e3fb4' }, // purple_dark
'#57b1be', // teal { name: 'teal', color: '#57b1be' }, // teal
'#847a8c' // gray { name: 'gray', color: '#847a8c' } // gray
] ]

View File

@ -14,7 +14,7 @@
// //
import { AttachedData, Class, Client, Doc, FindResult, Ref, Hierarchy } from '@hcengineering/core' import { AttachedData, Class, Client, Doc, FindResult, Ref, Hierarchy } from '@hcengineering/core'
import { IconSize } from '@hcengineering/ui' import { IconSize, ColorDefinition } from '@hcengineering/ui'
import { MD5 } from 'crypto-js' import { MD5 } from 'crypto-js'
import { Channel, Contact, contactPlugin, Person } from '.' import { Channel, Contact, contactPlugin, Person } from '.'
import { AVATAR_COLORS, GravatarPlaceholderType } from './types' import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
@ -22,14 +22,29 @@ import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
/** /**
* @public * @public
*/ */
export function getAvatarColorForId (id: string): string { export function getAvatarColorForId (id: string | null | undefined): string {
if (id == null) return AVATAR_COLORS[0].color
let hash = 0 let hash = 0
for (let i = 0; i < id.length; i++) { for (let i = 0; i < id.length; i++) {
hash += id.charCodeAt(i) hash += id.charCodeAt(i)
} }
return AVATAR_COLORS[hash % AVATAR_COLORS.length] return AVATAR_COLORS[hash % AVATAR_COLORS.length].color
}
/**
* @public
*/
export function getAvatarColors (): readonly ColorDefinition[] {
return AVATAR_COLORS
}
/**
* @public
*/
export function getAvatarColorName (color: string): string {
return AVATAR_COLORS.find((col) => col.color === color)?.name ?? AVATAR_COLORS[0].name
} }
/** /**

View File

@ -126,7 +126,7 @@
<div class="mr-2"> <div class="mr-2">
<Button icon={IconAdd} kind={'list'} on:click={createChild} /> <Button icon={IconAdd} kind={'list'} on:click={createChild} />
</div> </div>
<Avatar size={'medium'} avatar={value.avatar} icon={hr.icon.Department} /> <Avatar size={'medium'} avatar={value.avatar} icon={hr.icon.Department} name={value.name} />
<div class="flex-row ml-2 mr-4"> <div class="flex-row ml-2 mr-4">
<div class="fs-title"> <div class="fs-title">
{value.name} {value.name}

View File

@ -74,7 +74,6 @@
{#key object} {#key object}
<EditableAvatar <EditableAvatar
avatar={object.avatar} avatar={object.avatar}
id={object._id}
size={'x-large'} size={'x-large'}
icon={hr.icon.Department} icon={hr.icon.Department}
bind:this={avatarEditor} bind:this={avatarEditor}

View File

@ -34,7 +34,7 @@
<DocNavLink object={value}> <DocNavLink object={value}>
<div class="flex-row-center"> <div class="flex-row-center">
<div class="member-icon mr-2"> <div class="member-icon mr-2">
<Avatar size={'medium'} avatar={value.avatar} /> <Avatar size={'medium'} avatar={value.avatar} name={value.name} />
</div> </div>
<div class="flex-col"> <div class="flex-col">
<div class="member-title fs-title"> <div class="member-title fs-title">

View File

@ -189,8 +189,8 @@
<div class="ml-4 flex"> <div class="ml-4 flex">
<EditableAvatar <EditableAvatar
avatar={object.avatar} avatar={object.avatar}
id={customerId}
size={'large'} size={'large'}
name={object.name}
bind:this={avatarEditor} bind:this={avatarEditor}
bind:direct={avatar} bind:direct={avatar}
/> />

View File

@ -184,7 +184,7 @@
<div class="flex-between header bottom-divider"> <div class="flex-between header bottom-divider">
<div class="flex-row-center"> <div class="flex-row-center">
{#if employee} {#if employee}
<Avatar size="smaller" avatar={employee.avatar} /> <Avatar size={'smaller'} avatar={employee.avatar} name={employee.name} />
<span class="font-medium mx-2">{getName(client.getHierarchy(), employee)}</span> <span class="font-medium mx-2">{getName(client.getHierarchy(), employee)}</span>
{/if} {/if}
{#if newTxes > 0} {#if newTxes > 0}

View File

@ -88,7 +88,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="inbox-activity__content shrink flex-grow clear-mins" class:read={newTxes === 0}> <div class="inbox-activity__content shrink flex-grow clear-mins" class:read={newTxes === 0}>
<div class="flex-row-center gap-2"> <div class="flex-row-center gap-2">
<Avatar avatar={employee?.avatar} size="small" /> <Avatar avatar={employee?.avatar} size={'small'} name={employee?.name} />
{#if employee} {#if employee}
<span class="font-medium">{getName(client.getHierarchy(), employee)}</span> <span class="font-medium">{getName(client.getHierarchy(), employee)}</span>
{:else} {:else}

View File

@ -118,7 +118,7 @@
<div class="msgactivity-container"> <div class="msgactivity-container">
{#if withAvatar} {#if withAvatar}
<div class="msgactivity-avatar"> <div class="msgactivity-avatar">
<Avatar avatar={employee?.avatar} size="x-small" /> <Avatar avatar={employee?.avatar} size={'x-small'} name={employee?.name} />
</div> </div>
{:else} {:else}
<div class="msgactivity-icon"> <div class="msgactivity-icon">

View File

@ -46,7 +46,7 @@
<div class="antiContactCard"> <div class="antiContactCard">
<div class="label uppercase"><Label label={recruit.string.Talent} /></div> <div class="label uppercase"><Label label={recruit.string.Talent} /></div>
<Avatar avatar={candidate?.avatar} size={'large'} /> <Avatar avatar={candidate?.avatar} size={'large'} name={candidate?.name} />
{#if candidate} {#if candidate}
<DocNavLink object={candidate} {disabled}> <DocNavLink object={candidate} {disabled}>
<div class="name lines-limit-2"> <div class="name lines-limit-2">

View File

@ -562,8 +562,8 @@
bind:this={avatarEditor} bind:this={avatarEditor}
bind:direct={object.avatar} bind:direct={object.avatar}
avatar={undefined} avatar={undefined}
id={object._id}
size={'large'} size={'large'}
name={combineName(object?.firstName?.trim() ?? '', object?.lastName?.trim() ?? '')}
/> />
</div> </div>
</div> </div>

View File

@ -61,7 +61,7 @@
{/if} {/if}
<div class="flex-between mb-1"> <div class="flex-between mb-1">
<div class="flex-row-center"> <div class="flex-row-center">
<Avatar avatar={object.$lookup?.attachedTo?.avatar} size={'medium'} /> <Avatar avatar={object.$lookup?.attachedTo?.avatar} size={'medium'} name={object.$lookup?.attachedTo?.name} />
<div class="flex-grow flex-col min-w-0 ml-2"> <div class="flex-grow flex-col min-w-0 ml-2">
<div class="fs-title over-underline lines-limit-2"> <div class="fs-title over-underline lines-limit-2">
{object.$lookup?.attachedTo ? getName(client.getHierarchy(), object.$lookup.attachedTo) : ''} {object.$lookup?.attachedTo ? getName(client.getHierarchy(), object.$lookup.attachedTo) : ''}

View File

@ -37,6 +37,7 @@
{#if value} {#if value}
<div class="flex persons"> <div class="flex persons">
{#each persons as p} {#each persons as p}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
class="flex-presenter" class="flex-presenter"
class:inline-presenter={inline} class:inline-presenter={inline}
@ -44,7 +45,7 @@
on:click={() => onClick(p)} on:click={() => onClick(p)}
> >
<div class="icon"> <div class="icon">
<Avatar size={'x-small'} avatar={p.avatar} /> <Avatar size={'x-small'} avatar={p.avatar} name={p.name} />
</div> </div>
</div> </div>
{/each} {/each}

View File

@ -95,8 +95,8 @@
<EditableAvatar <EditableAvatar
avatar={employee.avatar} avatar={employee.avatar}
email={account.email} email={account.email}
id={employee._id}
size={'x-large'} size={'x-large'}
name={employee.name}
bind:this={avatarEditor} bind:this={avatarEditor}
on:done={onAvatarDone} on:done={onAvatarDone}
/> />

View File

@ -233,7 +233,7 @@
class="ml-2" class="ml-2"
use:tooltip={{ label: getEmbeddedLabel(getContactName(client.getHierarchy(), participant)) }} use:tooltip={{ label: getEmbeddedLabel(getContactName(client.getHierarchy(), participant)) }}
> >
<Avatar size="small" avatar={participant.avatar} /> <Avatar size={'small'} avatar={participant.avatar} name={participant.name} />
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -23,7 +23,7 @@
<div class="root"> <div class="root">
<div class="icon"> <div class="icon">
<Avatar avatar={lead.avatar} size="medium" /> <Avatar avatar={lead.avatar} size={'medium'} name={lead.name} />
</div> </div>
<div class="textContainer"> <div class="textContainer">
<div class="title"> <div class="title">

View File

@ -38,7 +38,6 @@
> >
<div class="flex-center ml-2"> <div class="flex-center ml-2">
<div class="flex-no-shrink circles-mark" class:isDraggable><IconCircles size={'small'} /></div> <div class="flex-no-shrink circles-mark" class:isDraggable><IconCircles size={'small'} /></div>
!!!
</div> </div>
<div class="root flex flex-between items-center w-full p-2"> <div class="root flex flex-between items-center w-full p-2">

View File

@ -163,7 +163,10 @@
}} }}
> >
{#if employee} {#if employee}
<Component is={contact.component.Avatar} props={{ avatar: employee.avatar, size: 'medium' }} /> <Component
is={contact.component.Avatar}
props={{ avatar: employee.avatar, size: 'medium', name: employee.name }}
/>
{/if} {/if}
<div class="ml-2 flex-col"> <div class="ml-2 flex-col">
{#if account} {#if account}

View File

@ -707,7 +707,10 @@
class="cursor-pointer" class="cursor-pointer"
on:click|stopPropagation={() => showPopup(AccountPopup, {}, popupPosition)} on:click|stopPropagation={() => showPopup(AccountPopup, {}, popupPosition)}
> >
<Component is={contact.component.Avatar} props={{ avatar: employee?.avatar, size: 'small' }} /> <Component
is={contact.component.Avatar}
props={{ avatar: employee?.avatar, size: 'small', name: employee?.name }}
/>
</div> </div>
</div> </div>
</div> </div>