mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 02:19:18 +00:00
wip
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
[id]="account.id"
|
||||
[text]="account.name"
|
||||
[color]="account.avatarColor"
|
||||
size="small"
|
||||
aria-hidden="true"
|
||||
></bit-avatar>
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[id]="currentAccount.id"
|
||||
[text]="currentAccount.name"
|
||||
[color]="currentAccount.avatarColor"
|
||||
size="small"
|
||||
aria-hidden="true"
|
||||
></bit-avatar>
|
||||
</button>
|
||||
@@ -24,7 +23,6 @@
|
||||
<bit-avatar
|
||||
[text]="'…'"
|
||||
[color]="'#6795E8'"
|
||||
size="small"
|
||||
aria-hidden="true"
|
||||
class="[&>img]:tw-block"
|
||||
></bit-avatar>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of filteredUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="user | userName" [id]="user.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
@@ -40,7 +40,7 @@
|
||||
</tr>
|
||||
<tr *ngFor="let user of excludedUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="user | userName" [id]="user.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
@@ -64,7 +64,7 @@
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of filteredUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="user | userName" [id]="user.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of users">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="user | userName" [id]="user.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<div>
|
||||
@@ -50,7 +50,7 @@
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of users">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="user | userName" [id]="user.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[color]="u.avatarColor"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of users">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="user | userName" [id]="user.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
@@ -53,7 +53,7 @@
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of users">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="user | userName" [id]="user.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ng-template #userDisplay let-user>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<div class="tw-mr-6">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="user | userName" [id]="user.id"></bit-avatar>
|
||||
</div>
|
||||
<ng-container *ngIf="user.name; else emailOnly">
|
||||
<div class="tw-truncate" [title]="user.name + ' ' + user.email">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let item of users">
|
||||
<td width="30" bitCell>
|
||||
<bit-avatar [text]="item.user | userName" [id]="item.user.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="item.user | userName" [id]="item.user.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ item.user.email }}
|
||||
|
||||
@@ -207,7 +207,6 @@
|
||||
<td bitCell (click)="edit(u, organization)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[color]="u.avatarColor"
|
||||
@@ -254,7 +253,6 @@
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[color]="u.avatarColor"
|
||||
|
||||
@@ -228,7 +228,6 @@
|
||||
<td bitCell (click)="edit(u, organization)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[color]="u.avatarColor"
|
||||
@@ -267,7 +266,6 @@
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[color]="u.avatarColor"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar>
|
||||
<bit-avatar [text]="org.name" [id]="org.id" size="2xlarge"></bit-avatar>
|
||||
<app-account-fingerprint
|
||||
[fingerprintMaterial]="organizationId"
|
||||
[publicKeyBuffer]="publicKeyBuffer"
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[title]="c.name"
|
||||
text="{{ profile | userName }}"
|
||||
[color]="c.color"
|
||||
[border]="true"
|
||||
>
|
||||
</selectable-avatar>
|
||||
</ng-container>
|
||||
@@ -28,7 +27,7 @@
|
||||
'!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600':
|
||||
customColorSelected,
|
||||
}"
|
||||
class="tw-relative tw-flex tw-size-24 tw-cursor-pointer tw-place-content-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
|
||||
class="tw-relative tw-flex tw-size-16 tw-cursor-pointer tw-place-content-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
|
||||
[style.background-color]="customColor$ | async"
|
||||
>
|
||||
<i
|
||||
|
||||
@@ -47,6 +47,8 @@ export class ChangeAvatarDialogComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("colorPicker") colorPickerElement: ElementRef<HTMLElement>;
|
||||
|
||||
loading = false;
|
||||
|
||||
// change palette to new colors
|
||||
defaultColorPalette: NamedAvatarColor[] = [
|
||||
{ name: "brightBlue", color: "#16cbfc" },
|
||||
{ name: "green", color: "#94cc4b" },
|
||||
@@ -102,6 +104,7 @@ export class ChangeAvatarDialogComponent implements OnInit, OnDestroy {
|
||||
this.setSelection(this.customColor$.value);
|
||||
}
|
||||
|
||||
// does this get used anywhere?
|
||||
async generateAvatarColor() {
|
||||
Utils.stringToColor(this.profile.name.toString());
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="tw-col-span-12 @3xl:tw-col-span-6 tw-row-start-1 @3xl:tw-row-start-auto">
|
||||
<div class="tw-mb-3 tw-flex tw-align-middle tw-items-center">
|
||||
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" size="large">
|
||||
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" size="2xlarge">
|
||||
</dynamic-avatar>
|
||||
<button
|
||||
class="tw-ml-3"
|
||||
|
||||
@@ -19,12 +19,10 @@ import { AvatarModule } from "@bitwarden/components";
|
||||
<bit-avatar
|
||||
appStopClick
|
||||
[text]="text"
|
||||
size="xlarge"
|
||||
size="2xlarge"
|
||||
[text]="text"
|
||||
[color]="color"
|
||||
[border]="false"
|
||||
[id]="id"
|
||||
[border]="border"
|
||||
[title]="title"
|
||||
>
|
||||
</bit-avatar>
|
||||
@@ -46,9 +44,6 @@ export class SelectableAvatarComponent {
|
||||
@Input() color: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() border = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() selected = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
[text]="c | userName"
|
||||
[id]="c.granteeId"
|
||||
[color]="c.avatarColor"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
<span class="tw-inline-flex tw-gap-2">
|
||||
<a bitLink href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
|
||||
@@ -173,7 +172,6 @@
|
||||
[text]="c | userName"
|
||||
[id]="c.grantorId"
|
||||
[color]="c.avatarColor"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
<span class="tw-inline-flex tw-gap-2">
|
||||
<span>{{ c.email }}</span>
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Component, Input, OnDestroy } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { AvatarSizes } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
@@ -19,7 +19,6 @@ type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall";
|
||||
[text]="text"
|
||||
[size]="size"
|
||||
[color]="color$ | async"
|
||||
[border]="border"
|
||||
[id]="id"
|
||||
[title]="title"
|
||||
>
|
||||
@@ -27,9 +26,6 @@ type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall";
|
||||
</span>`,
|
||||
})
|
||||
export class DynamicAvatarComponent implements OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() border = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() id: string;
|
||||
@@ -41,7 +37,7 @@ export class DynamicAvatarComponent implements OnDestroy {
|
||||
@Input() title: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() size: SizeTypes = "default";
|
||||
@Input() size: AvatarSizes = "base";
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
color$ = this.avatarService.avatarColor$;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
[ngClass]="{ 'tw-text-muted': addable.disabled }"
|
||||
>
|
||||
<td bitCell class="tw-w-8">
|
||||
<bit-avatar [text]="addable.name" [id]="addable.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="addable.name" [id]="addable.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ addable.name }}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</ng-container>
|
||||
<ng-template bitRowDef let-row>
|
||||
<td bitCell width="30">
|
||||
<bit-avatar [text]="row.organizationName" [id]="row.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="row.organizationName" [id]="row.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell width="325">
|
||||
<div class="tw-flex tw-items-center tw-gap-4 tw-break-all">
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
<td bitCell (click)="edit(user)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="user | userName"
|
||||
[id]="user.userId"
|
||||
[color]="user.avatarColor"
|
||||
|
||||
@@ -134,7 +134,6 @@
|
||||
<td bitCell (click)="edit(providerId, user)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="user | userName"
|
||||
[id]="user.userId"
|
||||
[color]="user.avatarColor"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let p of providers">
|
||||
<td bitCell class="tw-w-1">
|
||||
<bit-avatar [text]="p.name" [id]="p.id" size="small"></bit-avatar>
|
||||
<bit-avatar [text]="p.name" [id]="p.id"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<a href="#" [routerLink]="['/providers', p.id]">{{ p.name }}</a>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-avatar [text]="provider.name" [id]="provider.id" size="large"></bit-avatar>
|
||||
<bit-avatar [text]="provider.name" [id]="provider.id" size="2xlarge"></bit-avatar> // aaa
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" bitFormButton bitButton buttonType="primary">
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<ng-template bitRowDef let-row>
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar size="small" [text]="row.name" class="tw-mr-3"></bit-avatar>
|
||||
<bit-avatar [text]="row.name" class="tw-mr-3"></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<button type="button" bitLink (click)="edit(row)">
|
||||
{{ row.name }}
|
||||
|
||||
24
libs/components/src/avatar/avatar.component.html
Normal file
24
libs/components/src/avatar/avatar.component.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<span [title]="title() || text()">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
pointer-events="none"
|
||||
[style.backgroundColor]="customBackgroundColor()"
|
||||
[class]="svgClass()"
|
||||
attr.viewBox="0 0 {{ svgSize }} {{ svgSize }}"
|
||||
>
|
||||
<text
|
||||
text-anchor="middle"
|
||||
y="50%"
|
||||
x="50%"
|
||||
dy="0.35em"
|
||||
pointer-events="auto"
|
||||
[attr.fill]="textColor()"
|
||||
[style.fontWeight]="svgFontWeight"
|
||||
[style.fontSize.px]="svgFontSize"
|
||||
[style.lineHeight.px]="16"
|
||||
font-family='Inter,"Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||
>
|
||||
{{ displayChars() }}
|
||||
</text>
|
||||
</svg>
|
||||
</span>
|
||||
@@ -1,73 +1,92 @@
|
||||
import { NgClass } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
ElementRef,
|
||||
inject,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall";
|
||||
export type AvatarSizes = "2xlarge" | "xlarge" | "large" | "base" | "small";
|
||||
|
||||
const SizeClasses: Record<SizeTypes, string[]> = {
|
||||
xlarge: ["tw-h-24", "tw-w-24", "tw-min-w-24"],
|
||||
large: ["tw-h-16", "tw-w-16", "tw-min-w-16"],
|
||||
default: ["tw-h-10", "tw-w-10", "tw-min-w-10"],
|
||||
small: ["tw-h-7", "tw-w-7", "tw-min-w-7"],
|
||||
xsmall: ["tw-h-6", "tw-w-6", "tw-min-w-6"],
|
||||
export type AvatarColors = "teal" | "coral" | "brand" | "green" | "purple";
|
||||
|
||||
const SizeClasses: Record<AvatarSizes, string[]> = {
|
||||
"2xlarge": ["tw-h-16", "tw-w-16", "tw-min-w-16"],
|
||||
xlarge: ["tw-h-14", "tw-w-14", "tw-min-w-14"],
|
||||
large: ["tw-h-11", "tw-w-11", "tw-min-w-11"],
|
||||
base: ["tw-h-8", "tw-w-8", "tw-min-w-8"],
|
||||
small: ["tw-h-6", "tw-w-6", "tw-min-w-6"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Avatars display a unique color that helps a user visually recognize their logged in account.
|
||||
* Palette avatar color options. Prefer using these over custom colors. These are chosen for
|
||||
* cohesion with the rest of our Bitwarden color palette and for accessibility color contrast.
|
||||
* We reference color variables defined in tw-theme.css to ensure the avatar color handles light and
|
||||
* dark mode.
|
||||
*/
|
||||
export const DefaultAvatarColors: Record<AvatarColors, string> = {
|
||||
teal: "tw-bg-bg-avatar-teal",
|
||||
coral: "tw-bg-bg-avatar-coral",
|
||||
brand: "tw-bg-bg-avatar-brand",
|
||||
green: "tw-bg-bg-avatar-green",
|
||||
purple: "tw-bg-bg-avatar-purple",
|
||||
};
|
||||
|
||||
/**
|
||||
* Hover colors for each default avatar color, for use when the avatar is interactive. We reference
|
||||
* color variables defined in tw-theme.css to ensure the avatar color handles light and
|
||||
* dark mode.
|
||||
*/
|
||||
const DefaultAvatarHoverColors: Record<AvatarColors, string> = {
|
||||
teal: "group-hover/avatar:tw-bg-bg-avatar-teal-hover",
|
||||
coral: "group-hover/avatar:tw-bg-bg-avatar-coral-hover",
|
||||
brand: "group-hover/avatar:tw-bg-bg-avatar-brand-hover",
|
||||
green: "group-hover/avatar:tw-bg-bg-avatar-green-hover",
|
||||
purple: "group-hover/avatar:tw-bg-bg-avatar-purple-hover",
|
||||
};
|
||||
|
||||
/**
|
||||
* Avatars display a background color that helps a user visually recognize their logged in account.
|
||||
*
|
||||
* A variance in color across the avatar component is important as it is used in Account Switching as a
|
||||
* visual indicator to recognize which of a personal or work account a user is logged into.
|
||||
* Color options include a pre-defined set of palette-approved colors, or users can select a
|
||||
* custom color. A variance in color across the avatar component is important as it is used in
|
||||
* Account Switching as a visual indicator to recognize which of a personal or work account a user
|
||||
* is logged into.
|
||||
*
|
||||
* Avatars can be static or interactive.
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-avatar",
|
||||
template: `
|
||||
<span [title]="title() || text()">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
pointer-events="none"
|
||||
[style.backgroundColor]="backgroundColor()"
|
||||
[ngClass]="classList()"
|
||||
attr.viewBox="0 0 {{ svgSize }} {{ svgSize }}"
|
||||
>
|
||||
<text
|
||||
text-anchor="middle"
|
||||
y="50%"
|
||||
x="50%"
|
||||
dy="0.35em"
|
||||
pointer-events="auto"
|
||||
[attr.fill]="textColor()"
|
||||
[style.fontWeight]="svgFontWeight"
|
||||
[style.fontSize.px]="svgFontSize"
|
||||
font-family='Inter,"Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||
>
|
||||
{{ displayChars() }}
|
||||
</text>
|
||||
</svg>
|
||||
</span>
|
||||
`,
|
||||
selector: "bit-avatar, button[bit-avatar], a[bit-avatar]",
|
||||
templateUrl: "avatar.component.html",
|
||||
imports: [NgClass],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
class: "tw-group/avatar",
|
||||
},
|
||||
// host directive for aria disabled states? check figma for disabled styles
|
||||
})
|
||||
export class AvatarComponent {
|
||||
private el = inject(ElementRef);
|
||||
/**
|
||||
* Whether to display a border around the avatar.
|
||||
* Background color for the avatar. Provide one of the AvatarColors, or a custom hex code.
|
||||
*
|
||||
* If no color is provided, a color will be generated based on the id or text.
|
||||
*/
|
||||
readonly border = input(false);
|
||||
readonly color = input<AvatarColors | string>();
|
||||
|
||||
/**
|
||||
* Custom background color for the avatar. If not provided, a color will be generated based on the id or text.
|
||||
*/
|
||||
readonly color = input<string>();
|
||||
|
||||
/**
|
||||
* Unique identifier used to generate a consistent background color. Takes precedence over text for color generation.
|
||||
* Unique identifier used to generate a consistent background color. Takes precedence over text
|
||||
* for color generation when a color is not provided.
|
||||
*/
|
||||
readonly id = input<string>();
|
||||
|
||||
/**
|
||||
* Text to display in the avatar. The first letters of words (up to 2 characters) will be shown.
|
||||
* Also used to generate background color if id is not provided.
|
||||
* Also used to generate background color if color and id are not provided.
|
||||
*/
|
||||
readonly text = input<string>();
|
||||
|
||||
@@ -79,36 +98,67 @@ export class AvatarComponent {
|
||||
/**
|
||||
* Size of the avatar.
|
||||
*/
|
||||
readonly size = input<SizeTypes>("default");
|
||||
readonly size = input<AvatarSizes>("base");
|
||||
|
||||
protected readonly svgCharCount = 2;
|
||||
protected readonly svgFontSize = 20;
|
||||
protected readonly svgFontWeight = 300;
|
||||
protected readonly svgSize = 48;
|
||||
protected readonly svgFontSize = 12;
|
||||
protected readonly svgFontWeight = 400;
|
||||
protected readonly svgSize = 32;
|
||||
|
||||
protected readonly classList = computed(() => {
|
||||
return ["tw-rounded-full"]
|
||||
protected readonly svgClass = computed(() => {
|
||||
return ["tw-rounded-full", "tw-border-solid", this.backgroundColorClass()]
|
||||
.concat(SizeClasses[this.size()] ?? [])
|
||||
.concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
|
||||
.concat(this.hasHoverEffects() ? this.interactiveSvgClasses() : []);
|
||||
});
|
||||
|
||||
protected readonly backgroundColor = computed(() => {
|
||||
const id = this.id();
|
||||
const upperCaseText = this.text()?.toUpperCase() ?? "";
|
||||
protected readonly hasHoverEffects = computed(() => {
|
||||
return this.el.nativeElement.nodeName === "BUTTON" || this.el.nativeElement.nodeName === "A";
|
||||
});
|
||||
|
||||
if (!Utils.isNullOrWhitespace(this.color())) {
|
||||
protected readonly usingCustomColor = computed(() => {
|
||||
if (Utils.isNullOrWhitespace(this.color())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const defaultColorKeys = Object.keys(DefaultAvatarColors) as AvatarColors[];
|
||||
return !defaultColorKeys.includes(this.color() as AvatarColors);
|
||||
});
|
||||
|
||||
protected readonly customBackgroundColor = computed(() => {
|
||||
if (this.usingCustomColor()) {
|
||||
return this.color()!;
|
||||
}
|
||||
|
||||
if (!Utils.isNullOrWhitespace(id)) {
|
||||
return Utils.stringToColor(id!.toString());
|
||||
return undefined;
|
||||
});
|
||||
|
||||
protected readonly backgroundColorClass = computed(() => {
|
||||
if (!this.usingCustomColor()) {
|
||||
return DefaultAvatarColors[(this.color() as AvatarColors) ?? this.avatarDefaultColorKey()];
|
||||
}
|
||||
|
||||
return Utils.stringToColor(upperCaseText);
|
||||
return "";
|
||||
});
|
||||
|
||||
protected readonly interactiveSvgClasses = computed(() => {
|
||||
if (!this.usingCustomColor()) {
|
||||
return [
|
||||
DefaultAvatarHoverColors[(this.color() as AvatarColors) ?? this.avatarDefaultColorKey()],
|
||||
];
|
||||
}
|
||||
|
||||
// awaiting design choice for custom color hover state
|
||||
return "";
|
||||
});
|
||||
|
||||
protected readonly textColor = computed(() => {
|
||||
return Utils.pickTextColorBasedOnBgColor(this.backgroundColor(), 135, true);
|
||||
const customBg = this.customBackgroundColor();
|
||||
|
||||
if (customBg) {
|
||||
return Utils.pickTextColorBasedOnBgColor(customBg, 135, true);
|
||||
} else {
|
||||
return "white";
|
||||
}
|
||||
});
|
||||
|
||||
protected readonly displayChars = computed(() => {
|
||||
@@ -144,4 +194,25 @@ export class AvatarComponent {
|
||||
const characters = str.match(/./gu);
|
||||
return characters != null ? characters.slice(0, count).join("") : "";
|
||||
}
|
||||
|
||||
readonly avatarDefaultColorKey = computed(() => {
|
||||
let magicString = "";
|
||||
const id = this.id();
|
||||
|
||||
if (!Utils.isNullOrWhitespace(id)) {
|
||||
magicString = id!.toString();
|
||||
} else {
|
||||
magicString = this.text()?.toUpperCase() ?? "";
|
||||
}
|
||||
|
||||
const colorKeys = Object.keys(DefaultAvatarColors) as AvatarColors[];
|
||||
|
||||
let hash = 0;
|
||||
for (let i = 0; i < magicString.length; i++) {
|
||||
hash = magicString.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
const index = Math.abs(hash) % colorKeys.length;
|
||||
return colorKeys[index];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,15 +16,23 @@ import { AvatarModule } from "@bitwarden/components";
|
||||
|
||||
## Size
|
||||
|
||||
### Large: 64px
|
||||
### 2XLarge
|
||||
|
||||
<Canvas of={stories.XXLarge} />
|
||||
|
||||
### XLarge
|
||||
|
||||
<Canvas of={stories.XLarge} />
|
||||
|
||||
### Large
|
||||
|
||||
<Canvas of={stories.Large} />
|
||||
|
||||
### Default: 48px
|
||||
### Base
|
||||
|
||||
<Canvas of={stories.Default} />
|
||||
|
||||
### Small 28px
|
||||
### Small
|
||||
|
||||
<Canvas of={stories.Small} />
|
||||
|
||||
@@ -41,14 +49,6 @@ priority:
|
||||
Use the user 'ID' field if `Name` is not defined.
|
||||
<Canvas of={stories.ColorByID} />
|
||||
|
||||
## Outline
|
||||
|
||||
If the avatar is displayed on one of the theme's `background` color variables or is interactive,
|
||||
display the avatar with a 1 pixel `secondary-600` border to meet WCAG AA graphic contrast guidelines
|
||||
for interactive elements.
|
||||
|
||||
<Canvas of={stories.Border} />
|
||||
|
||||
## Avatar as a button
|
||||
|
||||
The Avatar can be used as a button.
|
||||
@@ -63,5 +63,4 @@ When the avatar is used as a button, the following states should be used:
|
||||
|
||||
## Accessibility
|
||||
|
||||
Avatar background color should have 3.1:1 contrast with it’s background; or include the
|
||||
`secondary-600` border Avatar text should have 4.5:1 contrast with the avatar background color
|
||||
Avatar background color should have 3.1:1 contrast with its background.
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
args: {
|
||||
id: undefined,
|
||||
text: "Walt Walterson",
|
||||
size: "default",
|
||||
size: "base",
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
@@ -32,7 +32,21 @@ export const Default: Story = {
|
||||
};
|
||||
},
|
||||
args: {
|
||||
color: "#175ddc",
|
||||
color: "brand",
|
||||
},
|
||||
};
|
||||
|
||||
export const XXLarge: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
size: "2xlarge",
|
||||
},
|
||||
};
|
||||
|
||||
export const XLarge: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
size: "xlarge",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -50,20 +64,6 @@ export const Small: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const LightBackground: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
color: "#d2ffcf",
|
||||
},
|
||||
};
|
||||
|
||||
export const Border: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
border: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ColorByID: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
@@ -77,3 +77,40 @@ export const ColorByText: Story = {
|
||||
text: "Jason Doe",
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomColor: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
color: "#fbd9ff",
|
||||
},
|
||||
};
|
||||
|
||||
export const Button: Story = {
|
||||
render: (args) => {
|
||||
return {
|
||||
props: args,
|
||||
template: `
|
||||
<button bit-avatar ${formatArgsForCodeSnippet<AvatarComponent>(args)}></button>
|
||||
`,
|
||||
};
|
||||
},
|
||||
args: {
|
||||
color: "brand",
|
||||
},
|
||||
};
|
||||
|
||||
// color by text or id button story?
|
||||
|
||||
export const CustomColorButton: Story = {
|
||||
render: (args) => {
|
||||
return {
|
||||
props: args,
|
||||
template: `
|
||||
<button bit-avatar ${formatArgsForCodeSnippet<AvatarComponent>(args)}></button>
|
||||
`,
|
||||
};
|
||||
},
|
||||
args: {
|
||||
color: "#fbd9ff",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -312,6 +312,18 @@
|
||||
--color-bg-accent-tertiary-medium: var(--color-purple-100);
|
||||
--color-bg-accent-tertiary: var(--color-purple-600);
|
||||
|
||||
/* Avatar Background */
|
||||
--color-bg-avatar-teal: var(--color-teal-700);
|
||||
--color-bg-avatar-teal-hover: var(--color-teal-800);
|
||||
--color-bg-avatar-coral: var(--color-coral-700);
|
||||
--color-bg-avatar-coral-hover: var(--color-coral-800);
|
||||
--color-bg-avatar-brand: var(--color-brand-700);
|
||||
--color-bg-avatar-brand-hover: var(--color-brand-800);
|
||||
--color-bg-avatar-green: var(--color-green-700);
|
||||
--color-bg-avatar-green-hover: var(--color-green-800);
|
||||
--color-bg-avatar-purple: var(--color-purple-700);
|
||||
--color-bg-avatar-purple-hover: var(--color-purple-800);
|
||||
|
||||
/* Hover & Overlay */
|
||||
--color-bg-hover: rgba(var(--color-gray-950-rgb), 0.05);
|
||||
--color-bg-overlay: rgba(var(--color-gray-950-rgb), 0.3);
|
||||
@@ -502,6 +514,18 @@
|
||||
--color-bg-accent-tertiary-medium: var(--color-purple-900);
|
||||
--color-bg-accent-tertiary: var(--color-purple-600);
|
||||
|
||||
/* Avatar Background */
|
||||
--color-bg-avatar-teal: var(--color-teal-700);
|
||||
--color-bg-avatar-teal-hover: var(--color-teal-600);
|
||||
--color-bg-avatar-coral: var(--color-coral-700);
|
||||
--color-bg-avatar-coral-hover: var(--color-coral-600);
|
||||
--color-bg-avatar-brand: var(--color-brand-700);
|
||||
--color-bg-avatar-brand-hover: var(--color-brand-600);
|
||||
--color-bg-avatar-green: var(--color-green-700);
|
||||
--color-bg-avatar-green-hover: var(--color-green-600);
|
||||
--color-bg-avatar-purple: var(--color-purple-700);
|
||||
--color-bg-avatar-purple-hover: var(--color-purple-600);
|
||||
|
||||
/* Hover & Overlay */
|
||||
--color-bg-hover: rgba(var(--color-white-rgb), 0.05);
|
||||
--color-bg-overlay: rgba(var(--color-gray-950-rgb), 0.85);
|
||||
|
||||
@@ -115,6 +115,16 @@ module.exports = {
|
||||
"accent-tertiary-soft": "var(--color-bg-accent-tertiary-soft)",
|
||||
"accent-tertiary-medium": "var(--color-bg-accent-tertiary-medium)",
|
||||
"accent-tertiary": "var(--color-bg-accent-tertiary)",
|
||||
"avatar-teal": "var(--color-bg-avatar-teal)",
|
||||
"avatar-teal-hover": "var(--color-bg-avatar-teal-hover)",
|
||||
"avatar-coral": "var(--color-bg-avatar-coral)",
|
||||
"avatar-coral-hover": "var(--color-bg-avatar-coral-hover)",
|
||||
"avatar-brand": "var(--color-bg-avatar-brand)",
|
||||
"avatar-brand-hover": "var(--color-bg-avatar-brand-hover)",
|
||||
"avatar-green": "var(--color-bg-avatar-green)",
|
||||
"avatar-green-hover": "var(--color-bg-avatar-green-hover)",
|
||||
"avatar-purple": "var(--color-bg-avatar-purple)",
|
||||
"avatar-purple-hover": "var(--color-bg-avatar-purple-hover)",
|
||||
hover: "var(--color-bg-hover)",
|
||||
overlay: "var(--color-bg-overlay)",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user