import { NgClass } from "@angular/common"; import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; import { Utils } from "@bitwarden/common/platform/misc/utils"; type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall"; const SizeClasses: Record = { 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"], }; /** * Avatars display a unique 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. */ @Component({ selector: "bit-avatar", template: ` {{ displayChars() }} `, imports: [NgClass], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AvatarComponent { /** * Whether to display a border around the avatar. */ readonly border = input(false); /** * Custom background color for the avatar. If not provided, a color will be generated based on the id or text. */ readonly color = input(); /** * Unique identifier used to generate a consistent background color. Takes precedence over text for color generation. */ readonly id = input(); /** * 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. */ readonly text = input(); /** * Title attribute for the avatar. If not provided, falls back to the text value. */ readonly title = input(); /** * Size of the avatar. */ readonly size = input("default"); protected readonly svgCharCount = 2; protected readonly svgFontSize = 20; protected readonly svgFontWeight = 300; protected readonly svgSize = 48; protected readonly classList = computed(() => { return ["tw-rounded-full"] .concat(SizeClasses[this.size()] ?? []) .concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []); }); protected readonly backgroundColor = computed(() => { const id = this.id(); const upperCaseText = this.text()?.toUpperCase() ?? ""; if (!Utils.isNullOrWhitespace(this.color())) { return this.color()!; } if (!Utils.isNullOrWhitespace(id)) { return Utils.stringToColor(id!.toString()); } return Utils.stringToColor(upperCaseText); }); protected readonly textColor = computed(() => { return Utils.pickTextColorBasedOnBgColor(this.backgroundColor(), 135, true); }); protected readonly displayChars = computed(() => { const upperCaseText = this.text()?.toUpperCase() ?? ""; let chars = this.getFirstLetters(upperCaseText, this.svgCharCount); if (chars == null) { chars = this.unicodeSafeSubstring(upperCaseText, this.svgCharCount); } // If the chars contain an emoji, only show it. const emojiMatch = chars.match(Utils.regexpEmojiPresentation); if (emojiMatch) { chars = emojiMatch[0]; } return chars; }); private getFirstLetters(data: string, count: number): string | undefined { const parts = data.split(" "); if (parts.length > 1) { let text = ""; for (let i = 0; i < count; i++) { text += this.unicodeSafeSubstring(parts[i], 1); } return text; } return undefined; } private unicodeSafeSubstring(str: string, count: number) { const characters = str.match(/./gu); return characters != null ? characters.slice(0, count).join("") : ""; } }