Skip to content

feat: add NeAvatar component #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/assets/avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 100 additions & 0 deletions src/components/NeAvatar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<!--
Copyright (C) 2025 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<script lang="ts" setup>
import { faUser } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, ref, useSlots } from 'vue'

export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'

const slots = useSlots()

const {
size = 'md',
img = '',
initials = '',
squared = false,
alt = 'Avatar'
} = defineProps<{
size?: AvatarSize
img?: string
initials?: string
squared?: boolean
alt?: string
}>()

const avatarSizeClasses: Record<AvatarSize, string> = {
xs: 'size-6',
sm: 'size-8',
md: 'size-10',
lg: 'size-12',
xl: 'size-14',
'2xl': 'size-16',
'3xl': 'size-20',
'4xl': 'size-24'
}

const placeholderColorClasses = 'bg-gray-700 text-white dark:bg-gray-200 dark:text-gray-950'

const placeholderIconSizeClasses: Record<AvatarSize, string> = {
xs: 'size-3',
sm: 'size-4',
md: 'size-5',
lg: 'size-6',
xl: 'size-7',
'2xl': 'size-8',
'3xl': 'size-10',
'4xl': 'size-12'
}

const initialsSizeClasses: Record<AvatarSize, string> = {
xs: 'text-xs',
sm: 'text-sm',
md: 'text-base',
lg: 'text-xl',
xl: 'text-2xl',
'2xl': 'text-3xl',
'3xl': 'text-4xl',
'4xl': 'text-5xl'
}

const imageError = ref(false)

const hasPlaceholder = computed(() => slots.placeholder)

const placeholderContainerClasses = computed(
() =>
`flex items-center justify-center ${placeholderColorClasses} ${squared ? 'rounded-sm' : 'rounded-full'} ${avatarSizeClasses[size]}`
)

function setImageError() {
imageError.value = true
}
</script>
<template>
<div>
<img
v-if="img && !imageError"
:alt="alt"
:class="[avatarSizeClasses[size], squared ? 'rounded-sm' : 'rounded-full']"
:src="img"
@error="setImageError"
/>
<div v-else-if="!initials && hasPlaceholder">
<slot name="placeholder" />
</div>
<div v-else-if="(imageError || !img) && !initials" :class="placeholderContainerClasses">
<FontAwesomeIcon
:icon="faUser"
:class="[placeholderColorClasses, placeholderIconSizeClasses[size]]"
/>
</div>
<div v-else :class="[placeholderContainerClasses, 'font-medium']">
<div :class="initialsSizeClasses[size]">
{{ initials }}
</div>
</div>
</div>
</template>
17 changes: 12 additions & 5 deletions src/components/NeCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import NeSkeleton from './NeSkeleton.vue'
import NeInlineNotification from './NeInlineNotification.vue'
import NeDropdown, { type NeDropdownItem } from './NeDropdown.vue'
import { computed, useSlots } from 'vue'

const props = defineProps({
title: {
Expand Down Expand Up @@ -46,7 +47,13 @@ const props = defineProps({
}
})

defineEmits(['titleClick'])
const slots = useSlots()

const isHeaderShown = computed(() => {
return (
props.title || slots.title || props.icon?.length || slots.topRight || props.menuItems?.length
)
})
</script>

<template>
Expand All @@ -57,12 +64,12 @@ defineEmits(['titleClick'])
]"
>
<!-- header -->
<div class="flex justify-between">
<div v-if="isHeaderShown" class="flex justify-between">
<!-- title -->
<div class="mb-3 flex items-center gap-1">
<h3
v-if="title || $slots.title"
class="leading-6 font-semibold text-gray-900 dark:text-gray-50"
class="leading-6 font-medium text-gray-900 dark:text-gray-50"
>
<span v-if="title">
{{ title }}
Expand Down Expand Up @@ -94,8 +101,8 @@ defineEmits(['titleClick'])
</div>
</div>
<!-- description and content -->
<div class="flex flex-row items-center justify-between">
<div class="grow">
<div class="flex h-full flex-row items-center justify-between">
<div class="h-full grow">
<NeSkeleton v-if="loading" :lines="skeletonLines"></NeSkeleton>
<NeInlineNotification
v-else-if="errorTitle"
Expand Down
1 change: 1 addition & 0 deletions src/components/NeDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ watch(
]"
: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}`"
>
<slot name="menuHeader"></slot>
<template v-for="item in items" :key="item.id">
<!-- divider -->
<hr
Expand Down
111 changes: 44 additions & 67 deletions src/components/NeModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,48 @@

<script lang="ts" setup>
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { faXmark as fasXmark } from '@fortawesome/free-solid-svg-icons'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import NeButton, { type ButtonKind } from './NeButton.vue'
import NeRoundedIcon from './NeRoundedIcon.vue'
import type { PropType } from 'vue'
import { watch } from 'vue'
import { faXmark } from '@fortawesome/free-solid-svg-icons'

export type ModalKind = 'neutral' | 'info' | 'warning' | 'error' | 'success'
export type PrimaryButtonKind = 'primary' | 'danger'
export type ModalSize = 'md' | 'lg' | 'xl' | 'xxl'

defineProps({
visible: {
type: Boolean,
required: true
},
title: {
type: String,
default: ''
},
kind: {
type: String as PropType<ModalKind>,
default: 'neutral'
},
size: {
type: String as PropType<ModalSize>,
default: 'md'
},
primaryLabel: {
type: String,
default: ''
},
secondaryLabel: {
type: String,
default: ''
},
cancelLabel: {
type: String,
default: ''
},
primaryButtonKind: {
type: String as PropType<PrimaryButtonKind>,
default: 'primary'
},
primaryButtonDisabled: {
type: Boolean,
default: false
},
primaryButtonLoading: {
type: Boolean,
default: false
},
secondaryButtonKind: {
type: String as PropType<ButtonKind>,
default: 'secondary'
},
secondaryButtonDisabled: {
type: Boolean,
default: false
},
secondaryButtonLoading: {
type: Boolean,
default: false
},
closeAriaLabel: {
type: String,
required: true
}
})

const emit = defineEmits(['close', 'primaryClick', 'secondaryClick'])
const {
visible = true,
title = '',
kind = 'neutral',
size = 'md',
primaryLabel = '',
secondaryLabel = '',
cancelLabel = '',
primaryButtonKind = 'primary',
primaryButtonDisabled = false,
primaryButtonLoading = false,
secondaryButtonKind = 'secondary',
secondaryButtonDisabled = false,
secondaryButtonLoading = false
} = defineProps<{
visible?: boolean
title?: string
kind?: ModalKind
size?: ModalSize
primaryLabel?: string
secondaryLabel?: string
cancelLabel?: string
primaryButtonKind?: ButtonKind
primaryButtonDisabled?: boolean
primaryButtonLoading?: boolean
secondaryButtonKind?: ButtonKind
secondaryButtonDisabled?: boolean
secondaryButtonLoading?: boolean
closeAriaLabel: string
}>()

// add fontawesome icons
library.add(fasXmark)
const emit = defineEmits(['close', 'primaryClick', 'secondaryClick', 'show'])

const sizeStyle: Record<ModalSize, string> = {
md: 'sm:max-w-lg',
Expand All @@ -87,6 +55,15 @@ const sizeStyle: Record<ModalSize, string> = {
xxl: 'sm:max-w-6xl'
}

watch(
() => visible,
() => {
if (visible) {
emit('show')
}
}
)

function onClose() {
emit('close')
}
Expand Down Expand Up @@ -139,7 +116,7 @@ function onSecondaryClick() {
@click="onClose"
>
<span class="sr-only">{{ closeAriaLabel }}</span>
<FontAwesomeIcon :icon="['fas', 'xmark']" class="h-5 w-5" aria-hidden="true" />
<FontAwesomeIcon :icon="faXmark" class="h-5 w-5" aria-hidden="true" />
</button>
</div>
<div class="sm:flex sm:items-start">
Expand All @@ -153,7 +130,7 @@ function onSecondaryClick() {
<DialogTitle
v-if="title"
as="h3"
class="mb-4 text-base leading-6 font-semibold text-gray-900 dark:text-gray-50"
class="mb-4 text-base leading-6 font-medium text-gray-900 dark:text-gray-50"
>{{ title }}</DialogTitle
>
<div>
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export { default as NeHeading } from './components/NeHeading.vue'
export { default as NeListbox } from './components/NeListbox.vue'
export { default as NeDropdownFilter } from './components/NeDropdownFilter.vue'
export { default as NeSortDropdown } from './components/NeSortDropdown.vue'
export { default as NeAvatar } from './components/NeAvatar.vue'

// types export
export type { NeComboboxOption } from './components/NeCombobox.vue'
Expand All @@ -49,6 +50,7 @@ export type { FilterOption, FilterKind } from './components/NeDropdownFilter.vue
export type { RadioOption } from './components/NeRadioSelection.vue'
export type { SortEvent } from './components/NeTableHeadCell.vue'
export type { ModalKind, PrimaryButtonKind, ModalSize } from './components/NeModal.vue'
export type { AvatarSize } from './components/NeAvatar.vue'

// library functions export
export {
Expand Down
Loading
Loading