diff --git a/packages/components/index.ts b/packages/components/index.ts index bc6d7e31..c249eeaa 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -27,3 +27,4 @@ export * from './sidebar' export * from './textarea' export * from './tab-navigation' export * from './progress-stepper' +export * from './tag' diff --git a/packages/components/tag/index.ts b/packages/components/tag/index.ts new file mode 100644 index 00000000..69156075 --- /dev/null +++ b/packages/components/tag/index.ts @@ -0,0 +1,8 @@ +import { withInstall } from '@puik/utils' + +import Tag from './src/tag.vue' + +export const PuikTag = withInstall(Tag) +export default PuikTag + +export * from './src/tag' diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts new file mode 100644 index 00000000..5d229647 --- /dev/null +++ b/packages/components/tag/src/tag.ts @@ -0,0 +1,64 @@ +import { buildProps } from '@puik/utils' +import type { PuikTooltipPosition } from '@puik/components' +import type { ExtractPropTypes, PropType } from 'vue' +import type Tag from './tag.vue' + +export const tagColorsVariants = [ + 'neutral', + 'blue', + 'yellow', + 'green', + 'purple', +] as const + +export const tagSizeVariants = ['default', 'small'] as const + +export type PuikTagColorVariant = (typeof tagColorsVariants)[number] +export type PuikTagSizeVariant = (typeof tagSizeVariants)[number] + +export const tagProps = buildProps({ + id: { + type: String, + required: true, + default: undefined, + }, + content: { + type: String, + required: true, + default: undefined, + }, + variant: { + type: String, + required: false, + default: 'neutral', + }, + size: { + type: String, + required: false, + default: 'default', + }, + icon: { + type: String, + default: '', + required: false, + }, + closeable: { + type: Boolean, + required: false, + default: false, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + tooltipPosition: { + type: String as PropType, + Required: false, + default: 'bottom', + }, +} as const) + +export type TagProps = ExtractPropTypes + +export type TagInstance = InstanceType diff --git a/packages/components/tag/src/tag.vue b/packages/components/tag/src/tag.vue new file mode 100644 index 00000000..1e2aba20 --- /dev/null +++ b/packages/components/tag/src/tag.vue @@ -0,0 +1,49 @@ + + + diff --git a/packages/components/tag/stories/tag.stories.ts b/packages/components/tag/stories/tag.stories.ts new file mode 100644 index 00000000..eddf193b --- /dev/null +++ b/packages/components/tag/stories/tag.stories.ts @@ -0,0 +1,600 @@ +import { ref } from 'vue' +import { PuikIcon, PuikButton, tooltipPositions } from '@puik/components' +import { tagColorsVariants, tagSizeVariants } from '../src/tag' +import PuikTag from './../src/tag.vue' +import type { Meta, StoryFn, Args } from '@storybook/vue3' + +const tagColorsVariantsSummary = tagColorsVariants.join('|') +const tagSizeVariantsSummary = tagSizeVariants.join('|') +const tooltipPositionsSummary = tooltipPositions.join('|') + +export default { + title: 'Components/Tag', + component: PuikTag, + argTypes: { + id: { + description: + "Prop which will correspond to the component's html id attribute. NB: must not start with a number", + control: 'text', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + content: { + description: + 'The text content (NB: if its length is equal to or greater than 30 characters it will be truncated and will be displayed entirely on hover in a tooltip)', + control: 'text', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + size: { + description: 'Size variants of tag component (default, small)', + control: 'select', + options: tagSizeVariants, + table: { + type: { + summary: tagSizeVariantsSummary, + }, + defaultValue: { + summary: 'default', + }, + }, + }, + variant: { + description: + 'Color variants of tag component (neutral by default, blue, yellow, green, purple)', + control: 'select', + options: tagColorsVariants, + table: { + type: { + summary: tagColorsVariantsSummary, + }, + defaultValue: { + summary: 'neutral', + }, + }, + }, + icon: { + description: 'Material icon name', + control: 'text', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'none', + }, + }, + }, + tooltipPosition: { + description: + 'Position of the tooltip (NB: a tooltip appears if the content length is equal to or greater than 30 characters).', + control: 'select', + options: tooltipPositions, + table: { + type: { + summary: tooltipPositionsSummary, + }, + defaultValue: { + summary: 'neutral', + }, + }, + }, + closeable: { + description: + 'Add closeable feature for the tag component (close icon which trigger a close event to parent component)', + control: 'boolean', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + }, + }, + disabled: { + description: 'Disables the Tag component ', + control: 'boolean', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + }, + }, + }, + args: { + id: 'puik-tag-id', + content: 'default tag', + size: 'default', + variant: 'neutral', + icon: '', + closeable: false, + disabled: false, + tooltipPosition: 'bottom', + }, +} as Meta + +const DefaultTemplate: StoryFn = (args: Args) => ({ + components: { + PuikIcon, + PuikTag, + }, + setup() { + return { args } + }, + template: ``, +}) + +const HandleCloseEventTemplate: StoryFn = (args: Args) => ({ + components: { + PuikIcon, + PuikTag, + PuikButton, + }, + setup() { + const tags = ref([ + { + variant: 'neutral', + icon: 'home', + closeable: true, + disabled: true, + content: "can't close disabled", + }, + { + variant: 'neutral', + icon: 'home', + closeable: true, + disabled: false, + content: 'close me !', + }, + { + variant: 'blue', + icon: 'home', + closeable: true, + disabled: false, + content: 'close me !', + }, + { + variant: 'yellow', + icon: 'home', + closeable: true, + disabled: false, + content: 'close me !', + }, + { + variant: 'green', + icon: 'home', + closeable: true, + disabled: false, + content: 'close me !', + }, + { + variant: 'purple', + icon: 'home', + closeable: true, + disabled: false, + content: 'close me !', + }, + ]) + + const copyInitialTags = [...tags.value] + const handleCloseTag = (index: number) => { + tags.value.splice(index, 1) + } + const refreshTags = () => { + tags.value = [] + copyInitialTags.map((tag) => { + tags.value.push(tag) + }) + } + return { tags, args, handleCloseTag, refreshTags } + }, + template: ` +
+ +
+ + + Refresh + +`, +}) + +const ColorVariantsTemplate: StoryFn = (args: Args) => ({ + components: { + PuikIcon, + PuikTag, + }, + setup() { + const tags = ref([ + { + variant: 'neutral', + content: 'neutral tag', + }, + { + variant: 'blue', + content: 'blue tag', + }, + { + variant: 'yellow', + content: 'yellow tag', + }, + { + variant: 'green', + content: 'green tag', + }, + { + variant: 'purple', + content: 'purple tag', + }, + ]) + + return { tags, args } + }, + template: ` +
+ +
+`, +}) + +const SizeVariantsTemplate: StoryFn = (args: Args) => ({ + components: { + PuikIcon, + PuikTag, + }, + setup() { + return { args } + }, + template: ` + + +`, +}) + +const CloseableTemplate: StoryFn = (args: Args) => ({ + components: { + PuikIcon, + PuikTag, + }, + setup() { + return { args } + }, + template: ` + +`, +}) + +const DisabledTemplate: StoryFn = (args: Args) => ({ + components: { + PuikIcon, + PuikTag, + }, + setup() { + return { args } + }, + template: ` + +`, +}) + +const WithIconTemplate: StoryFn = (args: Args) => ({ + components: { + PuikIcon, + PuikTag, + }, + setup() { + return { args } + }, + template: ` + +`, +}) + +const WithLongTextTemplate: StoryFn = (args: Args) => ({ + components: { + PuikIcon, + PuikTag, + }, + setup() { + return { args } + }, + template: ` + +`, +}) + +export const Default = { + render: DefaultTemplate, + args: {}, + parameters: { + docs: { + source: { + code: ` + + + + + +
+
+ default tag +
+
+`, + language: 'html', + }, + }, + }, +} + +export const HandleCloseEvent = { + render: HandleCloseEventTemplate, + args: {}, + parameters: { + docs: { + source: { + code: ` + +
+ +
+ + +const handleCloseTag = (index: number) => { + // Do stuff before closing + // Then + tags.value.splice(index, 1) +} +`, + language: 'html', + }, + }, + }, +} + +export const ColorVariants = { + render: ColorVariantsTemplate, + args: {}, + parameters: { + docs: { + source: { + code: ` + + + + + + + +
+
+ {$variant} tag +
+
+`, + language: 'html', + }, + }, + }, +} + +export const SizeVariants = { + render: SizeVariantsTemplate, + args: {}, + parameters: { + docs: { + source: { + code: ` + + + + + + + +
+
+ {$size} tag +
+
+`, + language: 'html', + }, + }, + }, +} + +export const Closeable = { + render: CloseableTemplate, + args: {}, + parameters: { + docs: { + source: { + code: ` + + + + + + + +
+
+ closeable tag +
+
close
+
+`, + language: 'html', + }, + }, + }, +} + +export const Disabled = { + render: DisabledTemplate, + args: {}, + parameters: { + docs: { + source: { + code: ` + + + + + +
+
+ disabled tag +
+
+`, + language: 'html', + }, + }, + }, +} + +export const WithIcon = { + render: WithIconTemplate, + args: {}, + parameters: { + docs: { + source: { + code: ` + + + + + +
+
+ favorite +
+
with icon tag
+
+`, + language: 'html', + }, + }, + }, +} + +export const WithLongText = { + render: WithLongTextTemplate, + args: {}, + parameters: { + docs: { + source: { + code: ` + + + +`, + language: 'html', + }, + }, + }, +} diff --git a/packages/components/tag/style/css.ts b/packages/components/tag/style/css.ts new file mode 100644 index 00000000..766ee1bf --- /dev/null +++ b/packages/components/tag/style/css.ts @@ -0,0 +1,2 @@ +import '@puik/components/base/style/css' +import '@puik/theme/puik-tag.css' diff --git a/packages/components/tag/style/index.ts b/packages/components/tag/style/index.ts new file mode 100644 index 00000000..7666132b --- /dev/null +++ b/packages/components/tag/style/index.ts @@ -0,0 +1,2 @@ +import '@puik/components/base/style' +import '@puik/theme/src/tag.scss' diff --git a/packages/components/tag/test/tag.spec.ts b/packages/components/tag/test/tag.spec.ts new file mode 100644 index 00000000..9857a71e --- /dev/null +++ b/packages/components/tag/test/tag.spec.ts @@ -0,0 +1,64 @@ +import { mount } from '@vue/test-utils' +import { describe, it, expect } from 'vitest' +import PuikTag from '../src/tag.vue' +import type { MountingOptions, VueWrapper } from '@vue/test-utils' + +describe('Tag tests', () => { + let wrapper: VueWrapper + const findTag = () => wrapper.find('.puik-tag') + const findTagContent = () => wrapper.find('.puik-tag__content') + const findCloseBtn = () => wrapper.find('.puik-tag__close') + const findLeftIcon = () => wrapper.find('.puik-tag__icon') + + const factory = ( + propsData: Record = {}, + options: MountingOptions = {} + ) => { + wrapper = mount(PuikTag, { + props: { + ...propsData, + }, + ...options, + }) + } + + it('should be a vue instance', () => { + factory() + expect(wrapper).toBeTruthy() + }) + + it('as id prop value is "puik-tag-example", id html attribute of puik-tag should be "puik-tag-example"', () => { + factory({ id: 'puik-tag-example' }) + expect(findTag().attributes().id).toBe('puik-tag-example') + }) + + it('Tag text should be "content"', () => { + factory({ content: 'content' }) + expect(findTagContent().text()).toBe('content') + }) + + it('should display a blue version of the tag', () => { + factory({ variant: 'blue' }) + expect(findTag().classes()).toContain('puik-tag--blue') + }) + + it('should display a tag small version', () => { + factory({ size: 'small' }) + expect(findTag().classes()).toContain('puik-tag--small') + }) + + it('should display a tag version with left icon', () => { + factory({ icon: 'home' }) + expect(findLeftIcon().text()).toBe('home') + }) + + it('should display a tag closeable version', () => { + factory({ closeable: true }) + expect(findCloseBtn().text()).toBe('close') + }) + + it('should display a tag disabled version', () => { + factory({ disabled: true }) + expect(findTag().classes()).toContain('puik-tag--disabled') + }) +}) diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index ab24ac2e..ec2a5c35 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -18,7 +18,7 @@ export const tooltipProps = buildProps({ position: { type: String as PropType, required: false, - default: 'top', + default: 'bottom', }, isDisabled: { type: Boolean, diff --git a/packages/puik/component.ts b/packages/puik/component.ts index bc64f9b7..b38f492d 100644 --- a/packages/puik/component.ts +++ b/packages/puik/component.ts @@ -1,3 +1,4 @@ +import { PuikTag } from '@puik/components/tag' import { PuikTabNavigation, PuikTabNavigationGroupPanels, @@ -48,6 +49,7 @@ import type { Plugin } from 'vue' // prettier-ignore export default [ + PuikTag, PuikTabNavigationGroupPanels, PuikTabNavigationTitle, PuikTabNavigationGroupTitles, diff --git a/packages/theme/src/index.scss b/packages/theme/src/index.scss index 14e753de..b0391f42 100644 --- a/packages/theme/src/index.scss +++ b/packages/theme/src/index.scss @@ -40,3 +40,4 @@ @use 'tab-navigation-group-panels'; @use 'progress-stepper'; @use 'progress-stepper-step'; +@use 'tag'; diff --git a/packages/theme/src/tag.scss b/packages/theme/src/tag.scss new file mode 100644 index 00000000..f5d8a57a --- /dev/null +++ b/packages/theme/src/tag.scss @@ -0,0 +1,52 @@ +@use './common/typography.scss'; + +.puik-tag { + @extend .puik-body-small; + @apply max-w-fit font-bold flex items-center; + + &--default { + @apply px-2 py-[3px]; + } + &--small { + @apply px-1 py-0; + .puik-tag__close { + @apply p-[1px]; + } + } + &--neutral { + @apply bg-primary-300 text-primary; + } + &--blue { + @apply bg-ocean-blue-50 text-primary; + } + &--yellow { + @apply bg-amber-100 text-primary; + } + &--green { + @apply bg-green-50 text-primary; + } + &--purple { + @apply bg-purple-50 text-primary; + } + &--disabled { + @apply bg-primary-200 text-primary-500; + .puik-tag__close { + @apply cursor-not-allowed hover:bg-primary-200; + } + } + &__close { + @apply p-[3px] ml-auto cursor-pointer rounded-[50%] hover:bg-primary-400; + } + &__icon { + @apply mr-1; + } + &__content { + @apply max-w-[150px] pr-[2px] overflow-hidden whitespace-nowrap text-ellipsis; + & > .puik-tooltip { + @apply max-w-[150px] overflow-hidden whitespace-nowrap text-ellipsis; + } + } + &__content .puik-tooltip__wrapper { + @apply max-w-[150px] mr-1 pr-[2px] overflow-hidden whitespace-nowrap text-ellipsis; + } +} diff --git a/typings/global.d.ts b/typings/global.d.ts index 7252421a..df474081 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -1,6 +1,7 @@ // GlobalComponents for Volar declare module '@vue/runtime-core' { export interface GlobalComponents { + PuikTag: typeof import('@prestashopcorp/puik')['PuikTag'] PuikTabNavigationGroupPanels: typeof import('@prestashopcorp/puik')['PuikTabNavigationGroupPanels'] PuikTabNavigationTitle: typeof import('@prestashopcorp/puik')['PuikTabNavigationTitle'] PuikTabNavigationGroupTitles: typeof import('@prestashopcorp/puik')['PuikTabNavigationGroupTitles']