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,