70 lines
1.2 KiB
Vue
70 lines
1.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from "vue";
|
|
|
|
const props = withDefaults(defineProps<{
|
|
src?: string | null;
|
|
alt?: string;
|
|
size?: number;
|
|
}>(), {
|
|
src: "",
|
|
alt: "",
|
|
size: 48
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
error: [event: Event];
|
|
}>();
|
|
const failed = ref(false);
|
|
const style = computed(() => ({
|
|
width: `${props.size}px`,
|
|
height: `${props.size}px`
|
|
}));
|
|
|
|
watch(() => props.src, () => {
|
|
failed.value = false;
|
|
});
|
|
|
|
function handleError(event: Event) {
|
|
failed.value = true;
|
|
emit("error", event);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<span class="safe-avatar" :style="style">
|
|
<img
|
|
v-if="src && !failed"
|
|
:src="src"
|
|
:alt="alt"
|
|
loading="lazy"
|
|
decoding="async"
|
|
referrerpolicy="no-referrer"
|
|
@error="handleError"
|
|
/>
|
|
<span v-else class="safe-avatar__fallback"><slot /></span>
|
|
</span>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.safe-avatar {
|
|
display: inline-grid;
|
|
flex: 0 0 auto;
|
|
place-items: center;
|
|
overflow: hidden;
|
|
border-radius: 50%;
|
|
color: var(--accent-strong);
|
|
background: var(--accent-soft);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.safe-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.safe-avatar__fallback {
|
|
line-height: 1;
|
|
}
|
|
</style>
|