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(Chip): new component #886

Merged
merged 17 commits into from
Nov 19, 2023
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
19 changes: 19 additions & 0 deletions docs/components/content/examples/ChipExampleContentSlot.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<UChip size="md" position="bottom-right" inset :ui="{ base: '-mx-2 rounded-none ring-0', background: '' }">
<UAvatar
src="https://avatars.githubusercontent.com/u/739984?v=4"
alt="Avatar"
size="lg"
/>

<template #content>
<UAvatar
src="https://avatars.githubusercontent.com/in/80442?v=4"
alt="Avatar"
size="xs"
:ui="{ rounded: 'rounded-md' }"
class="shadow-md"
/>
</template>
</UChip>
</template>
19 changes: 19 additions & 0 deletions docs/components/content/examples/ChipExampleShow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup>
const items = [{
name: 'messages',
icon: 'i-heroicons-chat-bubble-oval-left',
count: 3
}, {
name: 'notifications',
icon: 'i-heroicons-bell',
count: 0
}]
</script>

<template>
<div class="flex gap-3">
<UChip v-for="{ name, icon, count } in items" :key="name" :show="count > 0">
<UButton :icon="icon" color="gray" />
</UChip>
</div>
</template>
134 changes: 134 additions & 0 deletions docs/content/2.elements/12.chip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
description: Display a chip indicator on any component.
navigation:
badge: New
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Chip.vue
---

## Usage

Wrap any component with the `Chip` component to display a chip indicator.

::component-card
---
code: >-

<UButton icon="i-heroicons-inbox" color="gray" />
---

#default
:u-button{icon="i-heroicons-inbox" color="gray"}
::

### Size

Use the `size` prop to change the size of the chip.

::component-card
---
props:
size: '2xl'
code: >-

<UButton icon="i-heroicons-inbox" color="gray" />
---

#default
:u-button{icon="i-heroicons-inbox" color="gray"}
::

### Color

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

::component-card
---
props:
color: 'red'
code: >-

<UButton icon="i-heroicons-inbox" color="gray" />
---

#default
:u-button{icon="i-heroicons-inbox" color="gray"}
::

### Position

Use the `position` prop to change the position of the chip.

::component-card
---
props:
position: 'bottom-right'
code: >-

<UButton icon="i-heroicons-inbox" color="gray" />
---

#default
:u-button{icon="i-heroicons-inbox" color="gray"}
::

### Text

Use the `text` prop to display text in the chip.

::component-card
---
props:
text: '3'
size: '2xl'
excludedProps:
- size
code: >-

<UButton icon="i-heroicons-inbox" color="gray" />
---

#default
:u-button{icon="i-heroicons-inbox" color="gray"}
::

### Show

Use the `show` prop to conditionally display the chip.

:component-example{component="chip-example-show"}

### Inset

Use the `inset` prop to display the chip inside the component. This is useful when dealing with rounded components.

::component-card
---
props:
inset: true
code: >-

<UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" alt="Avatar" />
---

#default
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" alt="Avatar"}
::

## Slots

### `content`

Use the `#content` slot to fully customize the chip.

:component-example{component="chip-example-content-slot"}

## Props

:component-props

## Config

:component-preset
6 changes: 6 additions & 0 deletions src/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ const safelistByComponent = {
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}],
chip: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}]
}

Expand Down
92 changes: 92 additions & 0 deletions src/runtime/components/elements/Chip.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<template>
<div :class="ui.wrapper" v-bind="attrs">
<slot />

<span v-if="show" :class="chipClass">
<slot name="content">
{{ text }}
</slot>
</span>
</div>
</template>

<script lang="ts">
import { defineComponent, computed, toRef } from 'vue'
import type { PropType } from 'vue'
import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { ChipSize, ChipColor, ChipPosition, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { chip } from '#ui/ui.config'

const config = mergeConfig<typeof chip>(appConfig.ui.strategy, appConfig.ui.chip, chip)

export default defineComponent({
inheritAttrs: false,
props: {
size: {
type: String as PropType<ChipSize>,
default: () => config.default.size,
validator (value: string) {
return Object.keys(config.size).includes(value)
}
},
color: {
type: String as PropType<ChipColor>,
default: () => config.default.color,
validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value)
}
},
position: {
type: String as PropType<ChipPosition>,
default: () => config.default.position,
validator (value: string) {
return Object.keys(config.position).includes(value)
}
},
text: {
type: [String, Number],
default: null
},
inset: {
type: Boolean,
default: () => config.default.inset
},
show: {
type: Boolean,
default: true
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props) {
const { ui, attrs } = useUI('chip', toRef(props, 'ui'), config, toRef(props, 'class'))

const chipClass = computed(() => {
return twJoin(
ui.value.base,
ui.value.size[props.size],
ui.value.position[props.position],
props.inset ? null : ui.value.translate[props.position],
ui.value.background.replaceAll('{color}', props.color)
)
})

return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
chipClass
}
}
})
</script>
11 changes: 11 additions & 0 deletions src/runtime/types/chip.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { chip } from '../ui.config'
import colors from '#ui-colors'

export type ChipSize = keyof typeof chip.size
export type ChipColor = 'gray' | typeof colors[number]
export type ChipPosition = keyof typeof chip.position

export interface Chip {
color?: ChipColor
position?: ChipPosition
}
1 change: 1 addition & 0 deletions src/runtime/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './avatar'
export * from './badge'
export * from './breadcrumb'
export * from './button'
export * from './chip'
export * from './clipboard'
export * from './command-palette'
export * from './dropdown'
Expand Down
35 changes: 35 additions & 0 deletions src/runtime/ui.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,41 @@ export const buttonGroup = {
shadow: 'shadow-sm'
}

export const chip = {
wrapper: 'relative inline-flex items-center justify-center flex-shrink-0',
base: 'absolute rounded-full ring-1 ring-white dark:ring-gray-900 flex items-center justify-center text-white dark:text-gray-900 font-medium whitespace-nowrap',
background: 'bg-{color}-500 dark:bg-{color}-400',
position: {
'top-right': 'top-0 right-0',
'bottom-right': 'bottom-0 right-0',
'top-left': 'top-0 left-0',
'bottom-left': 'bottom-0 left-0'
},
translate: {
'top-right': '-translate-y-1/2 translate-x-1/2 transform',
'bottom-right': 'translate-y-1/2 translate-x-1/2 transform',
'top-left': '-translate-y-1/2 -translate-x-1/2 transform',
'bottom-left': 'translate-y-1/2 -translate-x-1/2 transform'
},
size: {
'3xs': 'h-[4px] min-w-[4px] text-[4px] p-px',
'2xs': 'h-[5px] min-w-[5px] text-[5px] p-px',
xs: 'h-1.5 min-w-[0.375rem] text-[6px] p-px',
sm: 'h-2 min-w-[0.5rem] text-[7px] p-0.5',
md: 'h-2.5 min-w-[0.625rem] text-[8px] p-0.5',
lg: 'h-3 min-w-[0.75rem] text-[10px] p-0.5',
xl: 'h-3.5 min-w-[0.875rem] text-[11px] p-1',
'2xl': 'h-4 min-w-[1rem] text-[12px] p-1',
'3xl': 'h-5 min-w-[1.25rem] text-[14px] p-1'
},
default: {
size: 'sm',
color: 'primary',
position: 'top-right',
inset: false
}
}

export const dropdown = {
wrapper: 'relative inline-flex text-left rtl:text-right',
container: 'z-20 group',
Expand Down