Skip to content

Commit

Permalink
feat(Badge): handle icon and avatar props (#2497)
Browse files Browse the repository at this point in the history
Co-authored-by: Benjamin Canac <[email protected]>
  • Loading branch information
malik-jouda and benjamincanac authored Nov 5, 2024
1 parent a97c511 commit 2d52834
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 50 deletions.
48 changes: 48 additions & 0 deletions docs/content/3.components/badge.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,54 @@ slots:
---
::

### Icon

Use the `icon` prop to show an [Icon](/components/icon) inside the Badge.

::component-code
---
props:
icon: i-heroicons-rocket-launch
size: md
color: primary
variant: solid
slots:
default: Badge
---
::

Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.

::component-code
---
props:
trailingIcon: i-heroicons-arrow-right
size: md
slots:
default: Badge
---
::

### Avatar

Use the `avatar` prop to show an [Avatar](/components/avatar) inside the Badge.

::component-code
---
prettier: true
props:
avatar:
src: 'https://github.com/nuxt.png'
size: md
color: neutral
variant: outline
slots:
default: |

Badge
---
::

## Examples

### `class` prop
Expand Down
27 changes: 25 additions & 2 deletions playground/app/pages/components/badge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,36 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
</UBadge>
</div>
<div class="flex items-center gap-2">
<UBadge v-for="variant in variants" :key="variant" :label="upperFirst(variant)" :variant="variant" />
<UBadge v-for="variant in variants" :key="variant" icon="i-heroicons-rocket-launch" :label="upperFirst(variant)" :variant="variant" />
</div>
<div class="flex items-center gap-2">
<UBadge v-for="variant in variants" :key="variant" :label="upperFirst(variant)" :variant="variant" color="neutral" />
<UBadge
v-for="variant in variants"
:key="variant"
icon="i-heroicons-rocket-launch"
:label="upperFirst(variant)"
:variant="variant"
color="neutral"
/>
</div>
<div class="flex items-center gap-2">
<UBadge
v-for="variant in variants"
:key="variant"
:avatar="{ src: 'https://github.com/benjamincanac.png' }"
:label="upperFirst(variant)"
:variant="variant"
color="neutral"
/>
</div>
<div class="flex items-center gap-2 ms-[-56px]">
<UBadge v-for="size in sizes" :key="size" label="Badge" :size="size" />
</div>
<div class="flex items-center gap-2 ms-[-86px]">
<UBadge v-for="size in sizes" :key="size" icon="i-heroicons-rocket-launch" label="Badge" :size="size" />
</div>
<div class="flex items-center gap-2 ms-[-86px]">
<UBadge v-for="size in sizes" :key="size" :avatar="{ src: 'https://github.com/benjamincanac.png' }" label="Badge" :size="size" />
</div>
</div>
</template>
33 changes: 30 additions & 3 deletions src/runtime/components/Badge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { tv, type VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/badge'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { badge: Partial<typeof theme> } }
const badge = tv({ extend: tv(theme), ...(appConfig.ui?.badge || {}) })
type BadgeVariants = VariantProps<typeof badge>
export interface BadgeProps {
export interface BadgeProps extends Omit<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
/**
* The element or component this component should render as.
* @defaultValue 'span'
Expand All @@ -21,26 +23,51 @@ export interface BadgeProps {
variant?: BadgeVariants['variant']
size?: BadgeVariants['size']
class?: any
ui?: Partial<typeof badge.slots>
}
export interface BadgeSlots {
leading(props?: {}): any
default(props?: {}): any
trailing(props?: {}): any
}
</script>

<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from 'radix-vue'
import { useComponentIcons } from '../composables/useComponentIcons'
import UIcon from './Icon.vue'
const props = withDefaults(defineProps<BadgeProps>(), {
as: 'span'
})
defineSlots<BadgeSlots>()
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
const ui = computed(() => badge({
color: props.color,
variant: props.variant,
size: props.size
}))
</script>

<template>
<Primitive :as="as" :class="badge({ color, variant, size, class: props.class })">
<Primitive :as="as" :class="ui.base({ class: [props.class, props.ui?.base] })">
<slot name="leading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
</slot>

<slot>
{{ label }}
<span v-if="label" :class="ui.label({ class: props.ui?.label })">
{{ label }}
</span>
</slot>

<slot name="trailing">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
</Primitive>
</template>
30 changes: 26 additions & 4 deletions src/theme/badge.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { ModuleOptions } from '../module'

export default (options: Required<ModuleOptions>) => ({
base: 'rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center',
slots: {
base: 'rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center',
label: 'truncate',
leadingIcon: 'shrink-0',
leadingAvatar: 'shrink-0',
leadingAvatarSize: '',
trailingIcon: 'shrink-0'
},
variants: {
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),
Expand All @@ -14,9 +21,24 @@ export default (options: Required<ModuleOptions>) => ({
subtle: ''
},
size: {
sm: 'text-xs px-1.5 py-0.5',
md: 'text-xs px-2 py-1',
lg: 'text-sm px-2 py-1'
sm: {
base: 'text-xs px-1.5 py-0.5 gap-1',
leadingIcon: 'size-4',
leadingAvatarSize: '3xs',
trailingIcon: 'size-4'
},
md: {
base: 'text-xs px-2 py-1 gap-1',
leadingIcon: 'size-4',
leadingAvatarSize: '3xs',
trailingIcon: 'size-4'
},
lg: {
base: 'text-sm px-2 py-1 gap-1.5',
leadingIcon: 'size-5',
leadingAvatarSize: '2xs',
trailingIcon: 'size-5'
}
}
},
compoundVariants: [...(options.theme.colors || []).map((color: string) => ({
Expand Down
12 changes: 11 additions & 1 deletion test/components/Badge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,18 @@ describe('Badge', () => {
...sizes.map((size: string) => [`with size ${size}`, { props: { label: 'Badge', size } }]),
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { label: 'Badge', variant } }]),
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { label: 'Badge', variant, color: 'neutral' } }]),
['with icon', { props: { icon: 'i-heroicons-rocket-launch' } }],
['with leading and icon', { props: { leading: true, icon: 'i-heroicons-arrow-left' } }],
['with leadingIcon', { props: { leadingIcon: 'i-heroicons-arrow-left' } }],
['with trailing and icon', { props: { trailing: true, icon: 'i-heroicons-arrow-right' } }],
['with trailingIcon', { props: { trailingIcon: 'i-heroicons-arrow-right' } }],
['with avatar', { props: { avatar: { src: 'https://github.com/benjamincanac.png' } } }],
['with avatar and leadingIcon', { props: { avatar: { src: 'https://github.com/benjamincanac.png' }, leadingIcon: 'i-heroicons-arrow-left' } }],
['with avatar and trailingIcon', { props: { avatar: { src: 'https://github.com/benjamincanac.png' }, trailingIcon: 'i-heroicons-arrow-right' } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }]
['with default slot', { slots: { default: () => 'Default slot' } }],
['with leading slot', { slots: { leading: () => 'Leading slot' } }],
['with trailing slot', { slots: { trailing: () => 'Trailing slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: BadgeProps, slots?: Partial<BadgeSlots> }) => {
const html = await ComponentRender(nameOrHtml, options, Badge)
expect(html).toMatchSnapshot()
Expand Down
Loading

0 comments on commit 2d52834

Please sign in to comment.