diff --git a/packages/vlossom/.storybook/examples/style-set.ts b/packages/vlossom/.storybook/examples/style-set.ts
index c54475908..1208974ae 100644
--- a/packages/vlossom/.storybook/examples/style-set.ts
+++ b/packages/vlossom/.storybook/examples/style-set.ts
@@ -8,6 +8,7 @@ import type {
VsValueTagStyleSet,
VsNoticeStyleSet,
VsProgressStyleSet,
+ VsChipStyleSet,
} from '@/components/types';
const vsButton: VsButtonStyleSet = {
@@ -15,6 +16,15 @@ const vsButton: VsButtonStyleSet = {
color: 'white',
};
+const vsChip: VsChipStyleSet = {
+ backgroundColor: '#a5d6ad',
+ borderRadius: '1.2rem',
+ color: '#304d30',
+ height: '3rem',
+ minHeight: '2rem',
+ padding: '0.8rem 1.5rem',
+};
+
const vsDivider: VsDividerStyleSet = {
lineColor: '#7071e8',
lineStyle: 'double',
@@ -61,6 +71,7 @@ const vsProgress: VsProgressStyleSet = {
export const styleSet: StyleSet = {
VsButton: { myStyleSet: vsButton },
+ VsChip: { myStyleSet: vsChip },
VsDivider: { myStyleSet: vsDivider },
VsInput: { myStyleSet: vsInput },
VsNotice: { myStyleSet: vsNotice },
diff --git a/packages/vlossom/src/assets/icons/check.ts b/packages/vlossom/src/assets/icons/check.ts
new file mode 100644
index 000000000..6437a15f8
--- /dev/null
+++ b/packages/vlossom/src/assets/icons/check.ts
@@ -0,0 +1,6 @@
+export default {
+ template: `
+ `,
+};
diff --git a/packages/vlossom/src/assets/icons/person.ts b/packages/vlossom/src/assets/icons/person.ts
new file mode 100644
index 000000000..72a15c26f
--- /dev/null
+++ b/packages/vlossom/src/assets/icons/person.ts
@@ -0,0 +1,6 @@
+export default {
+ template: `
+ `,
+};
diff --git a/packages/vlossom/src/components/types.ts b/packages/vlossom/src/components/types.ts
index f10756d95..fe497e115 100644
--- a/packages/vlossom/src/components/types.ts
+++ b/packages/vlossom/src/components/types.ts
@@ -1,4 +1,5 @@
export type * from './vs-button';
+export type * from './vs-chip';
export type * from './vs-container';
export type * from './vs-divider';
export type * from './vs-form';
diff --git a/packages/vlossom/src/components/vs-chip/VsChip.scss b/packages/vlossom/src/components/vs-chip/VsChip.scss
new file mode 100644
index 000000000..41cc6fd23
--- /dev/null
+++ b/packages/vlossom/src/components/vs-chip/VsChip.scss
@@ -0,0 +1,85 @@
+.vs-chip {
+ button {
+ all: unset;
+ }
+
+ align-items: center;
+ background-color: var(--vs-chip-backgroundColor, var(--vs-comp-backgroundColor));
+ border: var(--vs-chip-outlineBorder, 1px solid var(--vs-comp-color), 0);
+ border-radius: var(--vs-chip-borderRadius, 1.3rem);
+ color: var(--vs-chip-color, var(--vs-comp-color));
+ display: inline-flex;
+ font-size: var(--vs-chip-fontSize, 0.82rem);
+ font-weight: var(--vs-button-fontWeight, 400);
+ height: var(--vs-chip-height, 1.5rem);
+ justify-content: center;
+ min-height: var(--vs-chip-minHeight, 1.2rem);
+ padding: 0 0.4rem;
+ position: relative;
+
+ &:after {
+ background: #ffffff;
+ content: '';
+ display: block;
+ height: 100%;
+ left: 0;
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ transition: all 0.4s ease-out;
+ width: 120%;
+ }
+
+ .vs-chip-icon {
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .vs-chip-leading-icon {
+ overflow: hidden;
+ }
+
+ .vs-chip-close {
+ background-color: rgba(255, 255, 255, 0.7);
+ cursor: pointer;
+ height: calc(var(--vs-chip-height, 1.2rem) * 0.8);
+ width: calc(var(--vs-chip-height, 1.2rem) * 0.8);
+ }
+
+ .vs-chip-content {
+ padding: var(--vs-chip-padding, 0 0.2rem);
+ }
+
+ .vs-chip-icon + .vs-chip-content {
+ margin-left: 0.05rem;
+ }
+
+ &.noRound {
+ border-radius: 0.25rem;
+ .chip-icon {
+ border-radius: 0.25rem;
+ }
+ }
+
+ &.clickable {
+ cursor: pointer;
+
+ &:active:after {
+ opacity: 0.6;
+ transition: 0s;
+ width: 0%;
+ }
+ }
+
+ &.primary {
+ background-color: var(--vs-chip-backgroundColor, var(--vs-comp-backgroundColor-primary));
+ color: var(--vs-chip-color, var(--vs-comp-color-primary));
+ }
+}
+
+.vs-chip + .vs-chip {
+ margin-left: 0.2rem;
+}
diff --git a/packages/vlossom/src/components/vs-chip/VsChip.vue b/packages/vlossom/src/components/vs-chip/VsChip.vue
new file mode 100644
index 000000000..42faa668e
--- /dev/null
+++ b/packages/vlossom/src/components/vs-chip/VsChip.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/vlossom/src/components/vs-chip/__tests__/vs-chip.test.ts b/packages/vlossom/src/components/vs-chip/__tests__/vs-chip.test.ts
new file mode 100644
index 000000000..10f0d6a0a
--- /dev/null
+++ b/packages/vlossom/src/components/vs-chip/__tests__/vs-chip.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it, beforeEach } from 'vitest';
+import { mount } from '@vue/test-utils';
+import VsChip from './../VsChip.vue';
+
+function mountComponent() {
+ return mount(VsChip);
+}
+
+describe('vs-chip', () => {
+ describe('leading-icon', () => {
+ it('leading-icon 있는 경우 leading-icon 슬롯이 렌더된다', () => {
+ // given
+ const wrapper = mount(VsChip, {
+ slots: {
+ 'leading-icon': '
icon
',
+ },
+ });
+
+ // then
+ expect(wrapper.find('.vs-chip-leading-icon').exists()).toBe(true);
+ expect(wrapper.html()).toContain('icon
');
+ });
+ it('leading-icon이 없는 경우 leading-icon 슬롯이 렌더되지 않는다', () => {
+ // given
+ const wrapper = mount(VsChip);
+
+ // then
+ expect(wrapper.find('.vs-chip-leading-icon').exists()).toBe(false);
+ expect(wrapper.html()).not.toContain('icon
');
+ });
+ });
+
+ describe('closable', () => {
+ describe('closable 이 true 인 경우', () => {
+ let wrapper: ReturnType;
+
+ // given
+ beforeEach(() => {
+ wrapper = mount(VsChip, {
+ props: {
+ closable: true,
+ },
+ });
+ });
+
+ it('close 버튼이 렌더된다', () => {
+ // then
+ expect(wrapper.find('.vs-chip-close').exists()).toBe(true);
+ });
+
+ it('close 버튼이 눌렸을 때 close 함수를 emit 한다', async () => {
+ // when
+ const closeBtn = wrapper.find('.vs-chip-close');
+ await closeBtn.trigger('click');
+
+ // then
+ expect(wrapper.emitted()).toHaveProperty('close');
+ });
+ });
+
+ it('closable 이 false 인 경우 close 버튼이 렌더되지 않는다', () => {
+ // given
+ const wrapper = mount(VsChip, {
+ props: {
+ closable: false,
+ },
+ });
+
+ // then
+ expect(wrapper.find('.vs-chip-close').exists()).toBe(false);
+ });
+ });
+});
diff --git a/packages/vlossom/src/components/vs-chip/index.ts b/packages/vlossom/src/components/vs-chip/index.ts
new file mode 100644
index 000000000..e592da3a3
--- /dev/null
+++ b/packages/vlossom/src/components/vs-chip/index.ts
@@ -0,0 +1,12 @@
+import { VsComponent } from '@/declaration/types';
+import VsChip from './VsChip.vue';
+import type { VsChipStyleSet } from './VsChip.vue';
+
+type VsChipInstance = InstanceType;
+
+export type { VsChipInstance, VsChipStyleSet };
+
+export default {
+ name: VsComponent.VsChip,
+ component: VsChip,
+};
diff --git a/packages/vlossom/src/components/vs-chip/stories/VsChip.stories.ts b/packages/vlossom/src/components/vs-chip/stories/VsChip.stories.ts
new file mode 100644
index 000000000..7bafe61cb
--- /dev/null
+++ b/packages/vlossom/src/components/vs-chip/stories/VsChip.stories.ts
@@ -0,0 +1,190 @@
+import type { Meta, StoryObj } from '@storybook/vue3';
+import type { Ref } from 'vue';
+import { ref } from 'vue';
+import { colorScheme } from '@/storybook/args';
+import VsChip from './../VsChip.vue';
+import Check from '@/assets/icons/check';
+import Person from '@/assets/icons/person';
+
+const meta: Meta = {
+ title: 'Components/Base Components/VsChip',
+ component: VsChip,
+ render: (args: any) => ({
+ components: { VsChip },
+ setup() {
+ return { args };
+ },
+ template: 'Chip',
+ }),
+ tags: ['autodocs'],
+ argTypes: {
+ colorScheme,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const ColorScheme: Story = {
+ render: () => ({
+ components: { VsChip },
+ setup() {
+ const coloredChips = ref([...colorScheme.options]);
+ return { coloredChips };
+ },
+ template: `
+
+
+ Chip
+
+
+ `,
+ }),
+};
+
+export const Closable: Story = {
+ render: () => ({
+ components: { VsChip },
+ setup() {
+ const initialColoredChips = colorScheme.options.map((color, index) => ({ id: index, color: color }));
+ const coloredChips = ref(initialColoredChips);
+ const close = (index: number) => {
+ coloredChips.value = coloredChips.value.filter((_, i) => i !== index);
+ };
+ const reset = () => {
+ coloredChips.value = initialColoredChips;
+ };
+
+ return { coloredChips, close, reset };
+ },
+ template: `
+
+
+ Chip
+
+ Reset
+
+ `,
+ }),
+};
+
+export const NoRound: Story = {
+ render: () => ({
+ components: { VsChip },
+ setup() {
+ const coloredChips = ref([...colorScheme.options]);
+ return { coloredChips };
+ },
+ template: `
+
+
+ Chip
+
+
+ `,
+ }),
+};
+
+export const Primary: Story = {
+ render: () => ({
+ components: { VsChip },
+ setup() {
+ const coloredChips = ref([...colorScheme.options]);
+ return { coloredChips };
+ },
+ template: `
+
+
+ Chip
+
+
+ `,
+ }),
+};
+
+export const StyleSet: Story = {
+ args: {
+ styleSet: {
+ backgroundColor: '#1e88e5',
+ borderRadius: '0.2rem',
+ color: 'white',
+ fontSize: '1.2rem',
+ fontWeight: '500',
+ height: '2rem',
+ minHeight: '1rem',
+ outlineBorder: '3px solid #304d30',
+ padding: '0.8rem 1.5rem',
+ },
+ },
+};
+
+export const PreDefinedStyleSet: Story = {
+ args: {
+ styleSet: 'myStyleSet',
+ },
+};
+
+export const LeadingIcon: Story = {
+ render: () => ({
+ components: { VsChip, Check, Person },
+ template: `
+
+
+
+
+
+ Chip
+
+
+
+
+
+
+ Chip
+
+
+ `,
+ }),
+};
+
+export const ClickEventWithPrimary: Story = {
+ render: () => ({
+ components: { VsChip, Check },
+ setup() {
+ const coloredChips = ref(colorScheme.options.map((color, index) => ({ id: index, color: color })));
+
+ const selectedChips: Ref = ref([]);
+ const onClick = (id: number) => {
+ if (selectedChips.value.includes(id)) {
+ selectedChips.value = selectedChips.value.filter((chipId) => chipId !== id);
+ } else {
+ selectedChips.value.push(id);
+ }
+ };
+
+ const isSelected = (id: number) => selectedChips.value.includes(id);
+
+ return { coloredChips, selectedChips, onClick, isSelected };
+ },
+ template: `
+
+
+
+
+
+ Chip
+
+
+ `,
+ }),
+};
diff --git a/packages/vlossom/src/declaration/types.ts b/packages/vlossom/src/declaration/types.ts
index 5cc0548ac..40382692e 100644
--- a/packages/vlossom/src/declaration/types.ts
+++ b/packages/vlossom/src/declaration/types.ts
@@ -1,5 +1,6 @@
import type {
VsButtonStyleSet,
+ VsChipStyleSet,
VsDividerStyleSet,
VsInputStyleSet,
VsPageStyleSet,
@@ -12,6 +13,7 @@ import type { Ref } from 'vue';
export enum VsComponent {
VsButton = 'VsButton',
+ VsChip = 'VsChip',
VsContainer = 'VsContainer',
VsDivider = 'VsDivider',
VsForm = 'VsForm',
@@ -32,6 +34,7 @@ export type GlobalColorScheme = { default?: ColorScheme } & { [key in VsComponen
export interface StyleSet {
VsButton?: { [key: string]: VsButtonStyleSet };
+ VsChip?: { [key: string]: VsChipStyleSet };
VsDivider?: { [key: string]: VsDividerStyleSet };
VsInput?: { [key: string]: VsInputStyleSet };
VsSection?: { [key: string]: VsSectionStyleSet };