From 90586e0d94f1ebd2e3403c25dc49a80a295d8a76 Mon Sep 17 00:00:00 2001 From: Benjamin Canac <canacb1@gmail.com> Date: Fri, 4 Aug 2023 11:32:17 +0200 Subject: [PATCH] feat(Avatar): add `icon` prop as fallback --- docs/content/2.elements/3.avatar.md | 20 +++++++++- src/runtime/app.config.ts | 31 +++++++++++---- src/runtime/components/elements/Avatar.vue | 46 +++++++++++++++------- 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/docs/content/2.elements/3.avatar.md b/docs/content/2.elements/3.avatar.md index 44fa42b5e9..028dd51b94 100644 --- a/docs/content/2.elements/3.avatar.md +++ b/docs/content/2.elements/3.avatar.md @@ -51,9 +51,25 @@ baseProps: ### Placeholder -If there is an error loading the `src` of the avatar or `src` is null a background placeholder will be displayed, customizable in `ui.avatar.background`. +If there is an error loading the `src` of the avatar or `src` is null / false a background placeholder will be displayed, customizable in `ui.avatar.background`. -If there's an `alt` prop initials will be displayed on top of the background, customizable in `ui.avatar.placeholder`. +#### Icon + +You can use the `icon` prop to display an icon on top of the background, customizable in `ui.avatar.icon`. + +::component-card +--- +props: + icon: 'i-heroicons-photo' + size: 'sm' +excludedProps: + - icon +--- +:: + +#### Alt + +Otherwise, a placeholder will be displayed with the initials of the `alt` prop, customizable in `ui.avatar.placeholder`. ::component-card --- diff --git a/src/runtime/app.config.ts b/src/runtime/app.config.ts index 8b434f92f6..17e7cd3655 100644 --- a/src/runtime/app.config.ts +++ b/src/runtime/app.config.ts @@ -63,17 +63,18 @@ const avatar = { wrapper: 'relative inline-flex items-center justify-center', background: 'bg-gray-100 dark:bg-gray-800', rounded: 'rounded-full', - placeholder: 'font-medium leading-none text-gray-900 dark:text-white truncate', + text: 'font-medium leading-none text-gray-900 dark:text-white truncate', + placeholder: 'font-medium leading-none text-gray-500 dark:text-gray-400 truncate', size: { '3xs': 'h-4 w-4 text-[8px]', '2xs': 'h-5 w-5 text-[10px]', - xs: 'h-6 w-6 text-[11px]', - sm: 'h-8 w-8 text-xs', - md: 'h-10 w-10 text-sm', - lg: 'h-12 w-12 text-base', - xl: 'h-14 w-14 text-lg', - '2xl': 'h-16 w-16 text-xl', - '3xl': 'h-20 w-20 text-2xl' + xs: 'h-6 w-6 text-xs', + sm: 'h-8 w-8 text-sm', + md: 'h-10 w-10 text-base', + lg: 'h-12 w-12 text-lg', + xl: 'h-14 w-14 text-xl', + '2xl': 'h-16 w-16 text-2xl', + '3xl': 'h-20 w-20 text-3xl' }, chip: { base: 'absolute rounded-full ring-1 ring-white dark:ring-gray-900 flex items-center justify-center text-white dark:text-gray-900 font-medium', @@ -96,6 +97,20 @@ const avatar = { '3xl': 'h-5 min-w-[1.25rem] text-[14px] p-1' } }, + icon: { + base: 'text-gray-500 dark:text-gray-400 flex-shrink-0', + size: { + '3xs': 'h-2 w-2', + '2xs': 'h-2.5 w-2.5', + xs: 'h-3 w-3', + sm: 'h-4 w-4', + md: 'h-5 w-5', + lg: 'h-6 w-6', + xl: 'h-7 w-7', + '2xl': 'h-8 w-8', + '3xl': 'h-10 w-10' + } + }, default: { size: 'sm', chipColor: null, diff --git a/src/runtime/components/elements/Avatar.vue b/src/runtime/components/elements/Avatar.vue index 384eb44ca7..ebb0370ec4 100644 --- a/src/runtime/components/elements/Avatar.vue +++ b/src/runtime/components/elements/Avatar.vue @@ -6,9 +6,11 @@ :alt="alt" :src="url" v-bind="$attrs" - :onerror="() => onError()" + @error="onError" > - <span v-else-if="text || placeholder" :class="ui.placeholder">{{ text || placeholder }}</span> + <span v-else-if="text" :class="ui.text">{{ text }}</span> + <UIcon v-else-if="icon" :name="icon" :class="iconClass" /> + <span v-else-if="placeholder" :class="ui.placeholder">{{ placeholder }}</span> <span v-if="chipColor" :class="chipClass"> {{ chipText }} @@ -21,6 +23,7 @@ import { defineComponent, ref, computed, watch } from 'vue' import type { PropType } from 'vue' import { defu } from 'defu' +import UIcon from '../elements/Icon.vue' import { classNames } from '../../utils' import { useAppConfig } from '#imports' // TODO: Remove @@ -30,6 +33,9 @@ import appConfig from '#build/app.config' // const appConfig = useAppConfig() export default defineComponent({ + components: { + UIcon + }, inheritAttrs: false, props: { src: { @@ -44,6 +50,10 @@ export default defineComponent({ type: String, default: null }, + icon: { + type: String, + default: null + }, size: { type: String, default: () => appConfig.ui.avatar.default.size, @@ -80,10 +90,21 @@ export default defineComponent({ const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defu({}, props.ui, appConfig.ui.avatar)) + const url = computed(() => { + if (typeof props.src === 'boolean') { + return null + } + return props.src + }) + + const placeholder = computed(() => { + return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2) + }) + const wrapperClass = computed(() => { return classNames( ui.value.wrapper, - ui.value.background, + (error.value || !url.value) && ui.value.background, ui.value.rounded, ui.value.size[props.size] ) @@ -96,6 +117,13 @@ export default defineComponent({ ) }) + const iconClass = computed(() => { + return classNames( + ui.value.icon.base, + ui.value.icon.size[props.size] + ) + }) + const chipClass = computed(() => { return classNames( ui.value.chip.base, @@ -105,17 +133,6 @@ export default defineComponent({ ) }) - const url = computed(() => { - if (typeof props.src === 'boolean') { - return null - } - return props.src - }) - - const placeholder = computed(() => { - return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2) - }) - const error = ref(false) watch(() => props.src, () => { @@ -131,6 +148,7 @@ export default defineComponent({ return { wrapperClass, avatarClass, + iconClass, chipClass, url, placeholder,