Skip to content
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

feat/avatar-group #59

Merged
merged 5 commits into from
Mar 6, 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
3 changes: 2 additions & 1 deletion docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const modules = [
'OlDialog',
'OlDialogCard',
'OlTourProvider',
'OlTourStep'
'OlTourStep',
'OlAvatarGroup'
]

export default { ...theme, async enhanceApp({ app }) {
Expand Down
16 changes: 14 additions & 2 deletions docs/components/avatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ The `outlined` attribute allows you to display an outlined avatar.

<demo github="https://github.com/Onion-L/onionl-ui/tree/main/packages/components/avatar" vue="../demo/avatar/outline.vue" />

### Avatar Group

Avatar Group is used to display a group of avatars in a single component.It is particularly suitable for displaying a group of people or entities in a compact and visually appealing way.

<demo github="https://github.com/Onion-L/onionl-ui/tree/main/packages/components/avatar" vue="../demo/avatar/group.vue" />

## Attributes

| Attribute | Description | Type | Default |
Expand All @@ -62,9 +68,15 @@ The `outlined` attribute allows you to display an outlined avatar.
| clickable | Whether avatar is interactive | `boolean` | `false` |
| ariaLabel | ARIA label for accessibility | `string` | `-` |

### Avatar Group

| Attribute | Description | Type | Default |
| ------------- | --------------- | -------- | ------- |
| overlap | Overlap size between avatars | `string` | `30`|

### Events

| Name | Description | Parameters |
|------|-------------|------------|
| click | Triggered when clicked (only if clickable is true) | (event: MouseEvent) |
| error | Triggered when image fails to load | - |
| click | Triggered when clicked (only if clickable is true) | `(event: MouseEvent)` |
| error | Triggered when image fails to load | `-` |
12 changes: 12 additions & 0 deletions docs/demo/avatar/group.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div class="flex items-center">
<ol-avatar-group>
<ol-avatar icon="i-mi-user" background-color="green" />
<ol-avatar icon="i-mi-user" background-color="lightcoral" />
<ol-avatar icon="i-mi-user" background-color="lightseagreen" />
<ol-avatar icon="i-mi-user" background-color="lightsalmon" />
<ol-avatar icon="i-mi-user" background-color="lightsteelblue" />
<ol-avatar icon="i-mi-user" background-color="lightpink" />
</ol-avatar-group>
</div>
</template>
18 changes: 16 additions & 2 deletions docs/zh/components/avatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,16 @@

<demo github="https://github.com/Onion-L/onionl-ui/tree/main/packages/components/avatar" vue="../../demo/avatar/outline.vue" />

### 头像组

Avatar Group用于在单个组件中显示一组头像。特别适合以紧凑和视觉上吸引人的方式展示一组人或实体。

<demo github="https://github.com/Onion-L/onionl-ui/tree/main/packages/components/avatar" vue="../../demo/avatar/group.vue" />

## 属性

### Avatar

| 属性 | 描述 | 类型 | 默认值 |
| -------- | ----------- | ---- | ------- |
| src | 图像URL | `string` | `-` |
Expand All @@ -62,9 +70,15 @@
| clickable | 头像是否可交互 | `boolean` | `false` |
| ariaLabel | 辅助功能的ARIA标签 | `string` | `-` |

### Avatar Group

| 属性 | 描述 | 类型 | 默认值 |
| ------------- | --------------- | -------- | ------- |
| overlap | 头像之间的覆盖大小 | `string` | `30`|

### 事件

| 名称 | 描述 | 参数 |
|------|-------------|------------|
| click | 点击时触发(仅当`clickable`为true时) | (event: MouseEvent) |
| error | 图像加载失败时触发 | - |
| click | 点击时触发(仅当`clickable`为true时) | `(event: MouseEvent)` |
| error | 图像加载失败时触发 | `-` |
4 changes: 3 additions & 1 deletion packages/components/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useInstall } from '@onionl-ui/utils'
import Avatar from './src/avatar.vue'
import AvatarGroup from './src/group.vue'

export const OlAvatar = useInstall(Avatar)
export const OlAvatarGroup = useInstall(AvatarGroup)

export default OlAvatar
export default { OlAvatar, OlAvatarGroup }
export * from './src/avatar'
30 changes: 18 additions & 12 deletions packages/components/avatar/src/avatar.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import type { AvatarEmits, AvatarProps } from './avatar'
import { AVATAR_GROUP_OVERLAP } from '@onionl-ui/components/constant'
import { OlIcon } from '@onionl-ui/components/icon'
import { useNamespace } from '@onionl-ui/utils'
import initials from 'initials'
import { computed, ref } from 'vue'
import { computed, inject, ref } from 'vue'

defineOptions({
name: 'OlAvatar',
Expand All @@ -24,9 +26,11 @@ const ns = useNamespace('avatar')
const hasError = ref(false)
const DEFAULT_ICON = 'i-mi-user'

const overlap = inject<string | undefined>(AVATAR_GROUP_OVERLAP, undefined)

const isUsingNumericSize = computed(() => typeof Number(props.size) === 'number')

const outlineStyles = computed(() => {
const outlineStyles = computed<CSSProperties>(() => {
if (!props.outlined)
return {}

Expand All @@ -42,26 +46,28 @@ const outlineStyles = computed(() => {
}
})

const classes = computed(() => [
ns.namespace,
ns.m(props.shape),
isUsingNumericSize.value ? ns.m(props.size) : '',
{ [ns.m('clickable')]: props.clickable },
{ [ns.m('outlined')]: props.outlined },
])

const styles = computed(() => ({
const styles = computed<CSSProperties>(() => ({
backgroundColor: props.backgroundColor ?? 'var(--onl-primary)',
borderRadius: props.borderRadius ? `${props.borderRadius}px` : undefined,
cursor: props.clickable ? 'pointer' : 'default',
marginLeft: overlap ? `-${overlap}px` : 0,
...outlineStyles.value,
}))

const imgStyles = computed(() => ({
const imgStyles = computed<CSSProperties>(() => ({
width: isUsingNumericSize.value ? `${props.size}px` : '100%',
height: isUsingNumericSize.value ? `${props.size}px` : '100%',
}))

const classes = computed(() => [
ns.namespace,
ns.m(props.shape),
isUsingNumericSize.value ? ns.m(props.size) : '',
{ [ns.m('clickable')]: props.clickable },
{ [ns.m('outlined')]: props.outlined },
overlap ? ns.m('overlap') : '',
])

const fallbackContent = computed(() => {
return props.initials
? initials(props.initials)
Expand Down
36 changes: 36 additions & 0 deletions packages/components/avatar/src/group.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts" setup>
import { AVATAR_GROUP_OVERLAP } from '@onionl-ui/components/constant'
import { useNamespace } from '@onionl-ui/utils'
import { onMounted, provide, ref } from 'vue'

defineOptions({
name: 'OlAvatarGroup',
})

const props = defineProps({
overlap: {
type: String,
default: '10',

},
})

const groupRef = ref<HTMLElement | null>()

provide(AVATAR_GROUP_OVERLAP, props.overlap)

const ns = useNamespace('avatar')

onMounted(() => {
const firstChild = groupRef.value?.children[0] as HTMLElement
if (firstChild) {
firstChild.style.marginLeft = '0'
}
})
</script>

<template>
<div ref="groupRef" :class="[ns.b('group')]">
<slot />
</div>
</template>
1 change: 1 addition & 0 deletions packages/components/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const CLICK_EVENT = 'click'
export const CHANGE_EVENT = 'change'
export const TOUR_STEP_EVENT = Symbol('registerTourStep')
export const CONTEXTMENU_EVENT = Symbol('contextMenu')
export const AVATAR_GROUP_OVERLAP = Symbol('overlap')
4 changes: 2 additions & 2 deletions packages/components/draggableList/src/draggableItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ const props = defineProps<{
class?: HTMLAttributes['class']
}>()

const ns = useNamespace('draggable-item')
const ns = useNamespace('draggable')

const draggableItemCls = computed(() => {
return clsx(ns.namespace, props.class)
return clsx(ns.b('item'), props.class)
})
</script>

Expand Down
4 changes: 2 additions & 2 deletions packages/components/draggableList/src/draggableList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ const props = defineProps<{
class?: HTMLAttributes['class']
}>()

const ns = useNamespace('draggable-list')
const ns = useNamespace('draggable')

const draggableListCls = computed(() => {
return clsx(ns.namespace, props.class)
return clsx(ns.b('list'), props.class)
})

const DragListEl = ref<HTMLElement | null>(null)
Expand Down
4 changes: 2 additions & 2 deletions packages/components/text3d/src/text3d.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const props = withDefaults(defineProps<Text3DProps>(), {
layer: 5,
})

const ns = useNamespace('text-3d')
const ns = useNamespace('text')

const textShadow = computed(() => {
const layerCount = Math.abs(props.layer!)
Expand All @@ -31,7 +31,7 @@ const textShadow = computed(() => {
</script>

<template>
<div :class="ns.namespace" :style="{ textShadow }">
<div :class="ns.b('3d')" :style="{ textShadow }">
{{ text }}
</div>
</template>
Expand Down
50 changes: 50 additions & 0 deletions packages/components/tour/__test__/tour.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TOUR_STEP_EVENT } from '@onionl-ui/components/constant'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
Expand Down Expand Up @@ -128,4 +129,53 @@ describe('tour Component', () => {

expect(wrapper.vm.showTour).toBe(false)
})

it('should render slot content', () => {
const wrapper = mount(TourStep, {
props: { index: 1 },
slots: { default: '<div>test</div>' },
global: {
provide: {
[TOUR_STEP_EVENT]: vi.fn(),
},
},
})
expect(wrapper.html()).toContain('<div>test</div>')
})

it('should apply class attribute', () => {
const wrapper = mount(TourStep, {
props: { index: 1 },
attrs: { class: 'custom-class' },
global: {
provide: {
[TOUR_STEP_EVENT]: vi.fn(),
},
},
})
expect(wrapper.classes()).toContain('custom-class')
})

it('should display warning', () => {
const warn = vi.spyOn(console, 'warn')
mount(TourStep, {
props: { index: 1 },
})
expect(warn).toHaveBeenCalledWith(expect.stringContaining('OlTourProvider'))
})

it('should correctly register step', async () => {
const registerMock = vi.fn()
const wrapper = mount(TourStep, {
props: { index: 1, title: 'test', description: 'desc' },
global: {
provide: {
[TOUR_STEP_EVENT]: registerMock,
},
},
})

await wrapper.vm.$nextTick()
expect(registerMock).toHaveBeenCalledWith(1, expect.any(String), 'test', 'desc')
})
})
4 changes: 2 additions & 2 deletions packages/components/tour/src/step.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ const props = defineProps({
},
})

const ns = useNamespace('tour-step')
const ns = useNamespace('tour')
const stepRef = ref<HTMLElement | null>(null)
const uniqueClass = ns.m(`${props.index}`)
const uniqueClass = ns.bm('step', `${props.index}`)

const registerTourStep = inject<RegisterStep | null>(TOUR_STEP_EVENT, null)

Expand Down
3 changes: 2 additions & 1 deletion packages/onionl-ui/components.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Plugin } from 'vue'
import { OlAvatar } from '../components/avatar'
import { OlAvatar, OlAvatarGroup } from '../components/avatar'
import { OlButton } from '../components/button'
import { OlCard, OlCardContent, OlCardFooter, OlCardHeader } from '../components/card'
import { OlContextMenu, OlContextMenuItem } from '../components/contextMenu'
Expand All @@ -25,6 +25,7 @@ import { OlTypingText } from '../components/typing'

export const Components = [
OlAvatar,
OlAvatarGroup,
OlButton,
OlCard,
OlCardContent,
Expand Down
2 changes: 2 additions & 0 deletions preset/safelist/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const avatarSafelist = [
'ol-avatar-group',
'ol-avatar',
'ol-avatar--circle',
'ol-avatar--square',
Expand All @@ -11,4 +12,5 @@ export const avatarSafelist = [
'ol-avatar--xl',
'ol-avatar--2xl',
'ol-avatar--3xl',
'ol-avatar--overlap',
]
2 changes: 2 additions & 0 deletions preset/shortcuts/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Shortcut } from 'unocss'

export const avatarShortcuts: Shortcut[] = [
['ol-avatar-group', 'inline-flex items-center'],
['ol-avatar', 'inline-flex items-center justify-center overflow-hidden transition-all duration-200 ease aspect-square'],
['ol-avatar--overlap', 'hover:scale-120 hover:z-10 relative'],
['ol-avatar--clickable', 'hover:scale-105 hover:shadow-sm transition-transform duration-200'],
[/^ol-avatar--(circle|square)$/, ([, shape]) => `rounded-${shape}`],
[/^ol-avatar--(xs|sm|md|lg|xl|2xl|3xl)$/, ([, size]) => `w-avatar-${size} h-avatar-${size} text-avatar-${size}`],
Expand Down