Skip to content
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
21 changes: 21 additions & 0 deletions docs/content/docs/2.components/avatar-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ slots:
:u-avatar{src="https://github.com/noook.png" alt="Neil Richter" loading="lazy"}
::

### Color :badge{label="Soon" class="align-text-top"}

Use the `color` prop to change the color of all the avatars.

::component-code
---
prettier: true
props:
color: primary
slots:
default: |

<UAvatar alt="Benjamin Canac" />
<UAvatar alt="Romain Hamel" />
<UAvatar alt="Neil Richter" />
---
:u-avatar{alt="Benjamin Canac"}
:u-avatar{alt="Romain Hamel"}
:u-avatar{alt="Neil Richter"}
::

## Examples

### With tooltip
Expand Down
17 changes: 16 additions & 1 deletion docs/content/docs/2.components/avatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ props:
---
::


::note
You can pass any property from the HTML `<img>` element such as `alt`, `loading`, etc.
::
Expand All @@ -35,6 +34,8 @@ Use the `src` prop to set the image URL.

::component-code
---
ignore:
- loading
props:
src: 'https://github.com/benjamincanac.png'
loading: lazy
Expand All @@ -49,6 +50,7 @@ Use the `size` prop to set the size of the Avatar.
---
ignore:
- src
- loading
props:
src: 'https://github.com/benjamincanac.png'
size: xl
Expand Down Expand Up @@ -100,6 +102,18 @@ props:
The `alt` prop is passed to the `img` element as the `alt` attribute.
::

### Color :badge{label="Soon" class="align-text-top"}

Use the `color` prop to change the color of the Avatar.

::component-code
---
props:
color: primary
alt: 'Benjamin Canac'
---
::

### Chip

Use the `chip` prop to display a chip around the Avatar.
Expand All @@ -109,6 +123,7 @@ Use the `chip` prop to display a chip around the Avatar.
prettier: true
ignore:
- src
- loading
- chip.inset
props:
src: 'https://github.com/benjamincanac.png'
Expand Down
5 changes: 4 additions & 1 deletion playgrounds/nuxt/app/pages/components/avatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
import theme from '#build/ui/avatar'
const sizes = Object.keys(theme.variants.size)
const colors = Object.keys(theme.variants.color)
const attrs = reactive({
size: [theme.defaultVariants.size]
size: [theme.defaultVariants.size],
color: [theme.defaultVariants.color]
})
</script>

<template>
<Navbar>
<USelect v-model="attrs.size" :items="sizes" multiple />
<USelect v-model="attrs.color" :items="colors" multiple />
</Navbar>

<Matrix v-slot="props" :attrs="attrs">
Expand Down
9 changes: 7 additions & 2 deletions src/runtime/components/Avatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export interface AvatarProps extends /** @vue-ignore */ Omit<ImgHTMLAttributes,
* @defaultValue 'md'
*/
size?: Avatar['variants']['size']
/**
* @defaultValue 'neutral'
*/
color?: Avatar['variants']['color']
chip?: boolean | ChipProps
class?: any
style?: any
Expand Down Expand Up @@ -67,11 +71,12 @@ const fallback = computed(() => props.text || (props.alt || '').split(' ').map(w

const appConfig = useAppConfig() as Avatar['AppConfig']

const { size } = useAvatarGroup(_props)
const { size, color } = useAvatarGroup(_props)

// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.avatar || {}) })({
size: size.value ?? props.size
size: size.value ?? props.size,
color: color.value ?? props.color
}))

const rootClass = computed(() => ui.value.root({ class: [props.ui?.root, props.class] }))
Expand Down
10 changes: 8 additions & 2 deletions src/runtime/components/AvatarGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface AvatarGroupProps {
* @defaultValue 'md'
*/
size?: AvatarGroup['variants']['size']
/**
* @defaultValue 'neutral'
*/
color?: AvatarGroup['variants']['color']
/**
* The maximum number of avatars to display.
*/
Expand Down Expand Up @@ -47,7 +51,8 @@ const appConfig = useAppConfig() as AvatarGroup['AppConfig']

// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.avatarGroup || {}) })({
size: props.size
size: props.size,
color: props.color
}))

// eslint-disable-next-line vue/no-dupe-keys
Expand Down Expand Up @@ -94,7 +99,8 @@ const hiddenCount = computed(() => {
})

provide(avatarGroupInjectionKey, computed(() => ({
size: props.size
size: props.size,
color: props.color
})))
</script>

Expand Down
12 changes: 7 additions & 5 deletions src/runtime/composables/useAvatarGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@ import { inject, provide, computed } from 'vue'
import type { ComputedRef, InjectionKey } from 'vue'
import type { AvatarGroupProps } from '../types'

export const avatarGroupInjectionKey: InjectionKey<ComputedRef<{ size: AvatarGroupProps['size'] }>> = Symbol('nuxt-ui.avatar-group')
export const avatarGroupInjectionKey: InjectionKey<ComputedRef<{ size: AvatarGroupProps['size'], color: AvatarGroupProps['color'] }>> = Symbol('nuxt-ui.avatar-group')

/**
* Reads `size` from a wrapping `<UAvatarGroup>`.
* Reads `size` and `color` from a wrapping `<UAvatarGroup>`.
*
* **Always pass the raw `_props`, never the `useComponentProps` proxy** β€” the
* fallback `props.size ?? avatarGroup?.value.size` must keep the wrapping group
* winning over `<UTheme :props>`. To still apply theme defaults on bare avatars,
* fall back to the proxy at the `tv()` call site: `size: size.value ?? props.size`.
*/
export function useAvatarGroup(props: { size: AvatarGroupProps['size'] }) {
export function useAvatarGroup(props: { size: AvatarGroupProps['size'], color: AvatarGroupProps['color'] }) {
const avatarGroup = inject(avatarGroupInjectionKey, undefined)

const size = computed(() => props.size ?? avatarGroup?.value.size)
provide(avatarGroupInjectionKey, computed(() => ({ size: size.value })))
const color = computed(() => props.color ?? avatarGroup?.value.color)
provide(avatarGroupInjectionKey, computed(() => ({ size: size.value, color: color.value })))

return {
size
size,
color
}
}
13 changes: 10 additions & 3 deletions src/theme/avatar-group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default {
import type { ModuleOptions } from '../module'

export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'inline-flex flex-row-reverse justify-end',
base: 'relative rounded-full ring-bg first:me-0'
Expand Down Expand Up @@ -32,9 +34,14 @@ export default {
'3xl': {
base: 'ring-3 -me-2'
}
},
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),
neutral: ''
}
},
defaultVariants: {
size: 'md'
size: 'md',
color: 'neutral'
}
}
})
27 changes: 21 additions & 6 deletions src/theme/avatar.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
export default {
import type { ModuleOptions } from '../module'

export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated',
root: 'inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle',
image: 'h-full w-full rounded-[inherit] object-cover',
fallback: 'font-medium text-muted truncate',
icon: 'text-muted shrink-0'
fallback: 'font-medium truncate',
icon: 'shrink-0'
},
variants: {
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, {
root: `bg-${color}/10`,
fallback: `text-${color}`,
icon: `text-${color}`
}])),
neutral: {
root: 'bg-elevated',
fallback: 'text-muted',
icon: 'text-muted'
}
},
size: {
'3xs': {
root: 'size-4 text-[8px]'
Expand Down Expand Up @@ -37,6 +51,7 @@ export default {
}
},
defaultVariants: {
size: 'md'
size: 'md',
color: 'neutral'
}
}
})
2 changes: 2 additions & 0 deletions test/components/Avatar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import theme from '#build/ui/avatar'

describe('Avatar', () => {
const sizes = Object.keys(theme.variants.size) as any
const colors = Object.keys(theme.variants.color) as any

renderEach(Avatar, [
// Props
Expand All @@ -16,6 +17,7 @@ describe('Avatar', () => {
['with icon', { props: { icon: 'i-lucide-image' } }],
['with chip', { props: { chip: { text: '1' } } }],
...sizes.map((size: string) => [`with size ${size}`, { props: { src: 'https://github.com/benjamincanac.png', size } }]),
...colors.map((color: string) => [`with color ${color}`, { props: { alt: 'Benjamin Canac', color } }]),
['with as', { props: { as: 'section' } }],
['with as (object)', { props: { src: 'https://github.com/benjamincanac.png', as: { root: 'section', img: 'p' } } }],
['with as (partial object)', { props: { src: 'https://github.com/benjamincanac.png', as: { img: 'p' } } }],
Expand Down
2 changes: 2 additions & 0 deletions test/components/AvatarGroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ const AvatarGroupWrapper = defineComponent({

describe('AvatarGroup', () => {
const sizes = Object.keys(theme.variants.size) as any
const colors = Object.keys(theme.variants.color) as any

renderEach(AvatarGroupWrapper, [
// Props
['with max', { props: { max: 2 } }],
...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
...colors.map((color: string) => [`with color ${color}`, { props: { color, max: 1 } }]),
['with as', { props: { as: 'span' } }],
['with class', { props: { class: 'justify-start' } }],
['with ui', { props: { ui: { base: 'rounded-lg' } } }],
Expand Down
28 changes: 21 additions & 7 deletions test/components/__snapshots__/Avatar-vue.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Avatar > renders with alt correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium text-muted truncate">BC</span></span>"`;
exports[`Avatar > renders with alt correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-muted">BC</span></span>"`;

exports[`Avatar > renders with as (object) correctly 1`] = `
"<section data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base">
Expand All @@ -10,17 +10,31 @@ exports[`Avatar > renders with as (object) correctly 1`] = `

exports[`Avatar > renders with as (partial object) correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><p src="https://github.com/benjamincanac.png" width="32" height="32" data-slot="image" class="h-full w-full rounded-[inherit] object-cover"></p></span>"`;

exports[`Avatar > renders with as correctly 1`] = `"<section data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium text-muted truncate">&nbsp;</span></section>"`;
exports[`Avatar > renders with as correctly 1`] = `"<section data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-muted">&nbsp;</span></section>"`;

exports[`Avatar > renders with chip correctly 1`] = `"<span data-slot="root" class="relative inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium text-muted truncate">&nbsp;</span><span data-slot="base" class="rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap bg-primary h-[8px] min-w-[8px] text-[8px] top-0 right-0 absolute">1</span></span>"`;
exports[`Avatar > renders with chip correctly 1`] = `"<span data-slot="root" class="relative inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-muted">&nbsp;</span><span data-slot="base" class="rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap bg-primary h-[8px] min-w-[8px] text-[8px] top-0 right-0 absolute">1</span></span>"`;

exports[`Avatar > renders with class correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle size-8 text-base bg-default"><span data-slot="fallback" class="font-medium text-muted truncate">&nbsp;</span></span>"`;
exports[`Avatar > renders with class correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle size-8 text-base bg-default"><span data-slot="fallback" class="font-medium truncate text-muted">&nbsp;</span></span>"`;

exports[`Avatar > renders with color error correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-error/10 size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-error">BC</span></span>"`;

exports[`Avatar > renders with color info correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-info/10 size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-info">BC</span></span>"`;

exports[`Avatar > renders with color neutral correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-muted">BC</span></span>"`;

exports[`Avatar > renders with color primary correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-primary/10 size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-primary">BC</span></span>"`;

exports[`Avatar > renders with color secondary correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-secondary/10 size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-secondary">BC</span></span>"`;

exports[`Avatar > renders with color success correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-success/10 size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-success">BC</span></span>"`;

exports[`Avatar > renders with color warning correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-warning/10 size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-warning">BC</span></span>"`;

exports[`Avatar > renders with custom size correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated text-base size-100"><img src="https://github.com/benjamincanac.png" width="400" height="400" data-slot="image" class="h-full w-full rounded-[inherit] object-cover"></span>"`;

exports[`Avatar > renders with default slot correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base">πŸ‡«πŸ‡·</span>"`;

exports[`Avatar > renders with icon correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" data-slot="icon" class="text-muted shrink-0"></svg></span>"`;
exports[`Avatar > renders with icon correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" data-slot="icon" class="shrink-0 text-muted"></svg></span>"`;

exports[`Avatar > renders with size 2xl correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-11 text-[22px]"><img src="https://github.com/benjamincanac.png" width="44" height="44" data-slot="image" class="h-full w-full rounded-[inherit] object-cover"></span>"`;

Expand All @@ -42,6 +56,6 @@ exports[`Avatar > renders with size xs correctly 1`] = `"<span data-slot="root"

exports[`Avatar > renders with src correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><img src="https://github.com/benjamincanac.png" width="32" height="32" data-slot="image" class="h-full w-full rounded-[inherit] object-cover"></span>"`;

exports[`Avatar > renders with text correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium text-muted truncate">+1</span></span>"`;
exports[`Avatar > renders with text correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="font-medium truncate text-muted">+1</span></span>"`;

exports[`Avatar > renders with ui correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="text-muted truncate font-bold">&nbsp;</span></span>"`;
exports[`Avatar > renders with ui correctly 1`] = `"<span data-slot="root" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span data-slot="fallback" class="truncate text-muted font-bold">&nbsp;</span></span>"`;
Loading
Loading