Skip to content

Commit a0cee05

Browse files
andre8244CopilotTbaile
authored
feat: add NeAvatar component (#91)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tommaso Bailetti <tommaso.bailetti@nethesis.it>
1 parent fe7fc51 commit a0cee05

File tree

9 files changed

+294
-75
lines changed

9 files changed

+294
-75
lines changed

src/assets/avatar.png

133 KB
Loading

src/components/NeAvatar.vue

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<!--
2+
Copyright (C) 2025 Nethesis S.r.l.
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
-->
5+
<script lang="ts" setup>
6+
import { faUser } from '@fortawesome/free-solid-svg-icons'
7+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
8+
import { computed, ref, useSlots } from 'vue'
9+
10+
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
11+
12+
const slots = useSlots()
13+
14+
const {
15+
size = 'md',
16+
img = '',
17+
initials = '',
18+
squared = false,
19+
alt = 'Avatar'
20+
} = defineProps<{
21+
size?: AvatarSize
22+
img?: string
23+
initials?: string
24+
squared?: boolean
25+
alt?: string
26+
}>()
27+
28+
const avatarSizeClasses: Record<AvatarSize, string> = {
29+
xs: 'size-6',
30+
sm: 'size-8',
31+
md: 'size-10',
32+
lg: 'size-12',
33+
xl: 'size-14',
34+
'2xl': 'size-16',
35+
'3xl': 'size-20',
36+
'4xl': 'size-24'
37+
}
38+
39+
const placeholderColorClasses = 'bg-gray-700 text-white dark:bg-gray-200 dark:text-gray-950'
40+
41+
const placeholderIconSizeClasses: Record<AvatarSize, string> = {
42+
xs: 'size-3',
43+
sm: 'size-4',
44+
md: 'size-5',
45+
lg: 'size-6',
46+
xl: 'size-7',
47+
'2xl': 'size-8',
48+
'3xl': 'size-10',
49+
'4xl': 'size-12'
50+
}
51+
52+
const initialsSizeClasses: Record<AvatarSize, string> = {
53+
xs: 'text-xs',
54+
sm: 'text-sm',
55+
md: 'text-base',
56+
lg: 'text-xl',
57+
xl: 'text-2xl',
58+
'2xl': 'text-3xl',
59+
'3xl': 'text-4xl',
60+
'4xl': 'text-5xl'
61+
}
62+
63+
const imageError = ref(false)
64+
65+
const hasPlaceholder = computed(() => slots.placeholder)
66+
67+
const placeholderContainerClasses = computed(
68+
() =>
69+
`flex items-center justify-center ${placeholderColorClasses} ${squared ? 'rounded-sm' : 'rounded-full'} ${avatarSizeClasses[size]}`
70+
)
71+
72+
function setImageError() {
73+
imageError.value = true
74+
}
75+
</script>
76+
<template>
77+
<div>
78+
<img
79+
v-if="img && !imageError"
80+
:alt="alt"
81+
:class="[avatarSizeClasses[size], squared ? 'rounded-sm' : 'rounded-full']"
82+
:src="img"
83+
@error="setImageError"
84+
/>
85+
<div v-else-if="!initials && hasPlaceholder">
86+
<slot name="placeholder" />
87+
</div>
88+
<div v-else-if="(imageError || !img) && !initials" :class="placeholderContainerClasses">
89+
<FontAwesomeIcon
90+
:icon="faUser"
91+
:class="[placeholderColorClasses, placeholderIconSizeClasses[size]]"
92+
/>
93+
</div>
94+
<div v-else :class="[placeholderContainerClasses, 'font-medium']">
95+
<div :class="initialsSizeClasses[size]">
96+
{{ initials }}
97+
</div>
98+
</div>
99+
</div>
100+
</template>

src/components/NeCard.vue

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
88
import NeSkeleton from './NeSkeleton.vue'
99
import NeInlineNotification from './NeInlineNotification.vue'
1010
import NeDropdown, { type NeDropdownItem } from './NeDropdown.vue'
11+
import { computed, useSlots } from 'vue'
1112
1213
const props = defineProps({
1314
title: {
@@ -46,7 +47,13 @@ const props = defineProps({
4647
}
4748
})
4849
49-
defineEmits(['titleClick'])
50+
const slots = useSlots()
51+
52+
const isHeaderShown = computed(() => {
53+
return (
54+
props.title || slots.title || props.icon?.length || slots.topRight || props.menuItems?.length
55+
)
56+
})
5057
</script>
5158

5259
<template>
@@ -57,12 +64,12 @@ defineEmits(['titleClick'])
5764
]"
5865
>
5966
<!-- header -->
60-
<div class="flex justify-between">
67+
<div v-if="isHeaderShown" class="flex justify-between">
6168
<!-- title -->
6269
<div class="mb-3 flex items-center gap-1">
6370
<h3
6471
v-if="title || $slots.title"
65-
class="leading-6 font-semibold text-gray-900 dark:text-gray-50"
72+
class="leading-6 font-medium text-gray-900 dark:text-gray-50"
6673
>
6774
<span v-if="title">
6875
{{ title }}
@@ -94,8 +101,8 @@ defineEmits(['titleClick'])
94101
</div>
95102
</div>
96103
<!-- description and content -->
97-
<div class="flex flex-row items-center justify-between">
98-
<div class="grow">
104+
<div class="flex h-full flex-row items-center justify-between">
105+
<div class="h-full grow">
99106
<NeSkeleton v-if="loading" :lines="skeletonLines"></NeSkeleton>
100107
<NeInlineNotification
101108
v-else-if="errorTitle"

src/components/NeDropdown.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ watch(
101101
]"
102102
:class="`absolute z-50 mt-2.5 min-w-40 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-200 focus:outline-hidden dark:bg-gray-950 dark:ring-gray-700 ${menuClasses}`"
103103
>
104+
<slot name="menuHeader"></slot>
104105
<template v-for="item in items" :key="item.id">
105106
<!-- divider -->
106107
<hr

src/components/NeModal.vue

Lines changed: 44 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,80 +5,48 @@
55

66
<script lang="ts" setup>
77
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
8-
import { faXmark as fasXmark } from '@fortawesome/free-solid-svg-icons'
9-
import { library } from '@fortawesome/fontawesome-svg-core'
108
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
119
import NeButton, { type ButtonKind } from './NeButton.vue'
1210
import NeRoundedIcon from './NeRoundedIcon.vue'
13-
import type { PropType } from 'vue'
11+
import { watch } from 'vue'
12+
import { faXmark } from '@fortawesome/free-solid-svg-icons'
1413
1514
export type ModalKind = 'neutral' | 'info' | 'warning' | 'error' | 'success'
1615
export type PrimaryButtonKind = 'primary' | 'danger'
1716
export type ModalSize = 'md' | 'lg' | 'xl' | 'xxl'
1817
19-
defineProps({
20-
visible: {
21-
type: Boolean,
22-
required: true
23-
},
24-
title: {
25-
type: String,
26-
default: ''
27-
},
28-
kind: {
29-
type: String as PropType<ModalKind>,
30-
default: 'neutral'
31-
},
32-
size: {
33-
type: String as PropType<ModalSize>,
34-
default: 'md'
35-
},
36-
primaryLabel: {
37-
type: String,
38-
default: ''
39-
},
40-
secondaryLabel: {
41-
type: String,
42-
default: ''
43-
},
44-
cancelLabel: {
45-
type: String,
46-
default: ''
47-
},
48-
primaryButtonKind: {
49-
type: String as PropType<PrimaryButtonKind>,
50-
default: 'primary'
51-
},
52-
primaryButtonDisabled: {
53-
type: Boolean,
54-
default: false
55-
},
56-
primaryButtonLoading: {
57-
type: Boolean,
58-
default: false
59-
},
60-
secondaryButtonKind: {
61-
type: String as PropType<ButtonKind>,
62-
default: 'secondary'
63-
},
64-
secondaryButtonDisabled: {
65-
type: Boolean,
66-
default: false
67-
},
68-
secondaryButtonLoading: {
69-
type: Boolean,
70-
default: false
71-
},
72-
closeAriaLabel: {
73-
type: String,
74-
required: true
75-
}
76-
})
77-
78-
const emit = defineEmits(['close', 'primaryClick', 'secondaryClick'])
18+
const {
19+
visible = true,
20+
title = '',
21+
kind = 'neutral',
22+
size = 'md',
23+
primaryLabel = '',
24+
secondaryLabel = '',
25+
cancelLabel = '',
26+
primaryButtonKind = 'primary',
27+
primaryButtonDisabled = false,
28+
primaryButtonLoading = false,
29+
secondaryButtonKind = 'secondary',
30+
secondaryButtonDisabled = false,
31+
secondaryButtonLoading = false
32+
} = defineProps<{
33+
visible?: boolean
34+
title?: string
35+
kind?: ModalKind
36+
size?: ModalSize
37+
primaryLabel?: string
38+
secondaryLabel?: string
39+
cancelLabel?: string
40+
primaryButtonKind?: ButtonKind
41+
primaryButtonDisabled?: boolean
42+
primaryButtonLoading?: boolean
43+
secondaryButtonKind?: ButtonKind
44+
secondaryButtonDisabled?: boolean
45+
secondaryButtonLoading?: boolean
46+
closeAriaLabel: string
47+
}>()
7948
80-
// add fontawesome icons
81-
library.add(fasXmark)
49+
const emit = defineEmits(['close', 'primaryClick', 'secondaryClick', 'show'])
8250
8351
const sizeStyle: Record<ModalSize, string> = {
8452
md: 'sm:max-w-lg',
@@ -87,6 +55,15 @@ const sizeStyle: Record<ModalSize, string> = {
8755
xxl: 'sm:max-w-6xl'
8856
}
8957
58+
watch(
59+
() => visible,
60+
() => {
61+
if (visible) {
62+
emit('show')
63+
}
64+
}
65+
)
66+
9067
function onClose() {
9168
emit('close')
9269
}
@@ -139,7 +116,7 @@ function onSecondaryClick() {
139116
@click="onClose"
140117
>
141118
<span class="sr-only">{{ closeAriaLabel }}</span>
142-
<FontAwesomeIcon :icon="['fas', 'xmark']" class="h-5 w-5" aria-hidden="true" />
119+
<FontAwesomeIcon :icon="faXmark" class="h-5 w-5" aria-hidden="true" />
143120
</button>
144121
</div>
145122
<div class="sm:flex sm:items-start">
@@ -153,7 +130,7 @@ function onSecondaryClick() {
153130
<DialogTitle
154131
v-if="title"
155132
as="h3"
156-
class="mb-4 text-base leading-6 font-semibold text-gray-900 dark:text-gray-50"
133+
class="mb-4 text-base leading-6 font-medium text-gray-900 dark:text-gray-50"
157134
>{{ title }}</DialogTitle
158135
>
159136
<div>

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export { default as NeHeading } from './components/NeHeading.vue'
3838
export { default as NeListbox } from './components/NeListbox.vue'
3939
export { default as NeDropdownFilter } from './components/NeDropdownFilter.vue'
4040
export { default as NeSortDropdown } from './components/NeSortDropdown.vue'
41+
export { default as NeAvatar } from './components/NeAvatar.vue'
4142

4243
// types export
4344
export type { NeComboboxOption } from './components/NeCombobox.vue'
@@ -49,6 +50,7 @@ export type { FilterOption, FilterKind } from './components/NeDropdownFilter.vue
4950
export type { RadioOption } from './components/NeRadioSelection.vue'
5051
export type { SortEvent } from './components/NeTableHeadCell.vue'
5152
export type { ModalKind, PrimaryButtonKind, ModalSize } from './components/NeModal.vue'
53+
export type { AvatarSize } from './components/NeAvatar.vue'
5254

5355
// library functions export
5456
export {

0 commit comments

Comments
 (0)