mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
refactor: add Badge component and fix twMerge font-size detection (#10580)
## Summary - Rename `text-xxxs`/`text-xxs` to `text-3xs`/`text-2xs` in design system CSS — fixes `tailwind-merge` incorrectly classifying custom font-size utilities as color classes, which clobbered text color - Add `Badge` component with updated severity colors matching Figma design (white text on colored backgrounds) - Add Badge stories under `Components/Badges/Badge` - Add unit tests including twMerge regression coverage Split from #10438 per review feedback — this PR contains the foundational Badge component; migration of consumers follows in a separate PR. ## Test plan - [x] Unit tests pass (`Badge.test.ts` — 12 tests) - [x] Typecheck passes - [x] Lint passes - [ ] Verify Badge stories render correctly in Storybook - [ ] Verify existing components using `text-2xs`/`text-3xs` render unchanged Fixes #10438 (partial) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10580-refactor-add-Badge-component-and-fix-twMerge-font-size-detection-32f6d73d3650810dae7cd0d4af67fd1c) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -28,11 +28,8 @@
|
||||
--text-2xs: 0.625rem;
|
||||
--text-2xs--line-height: calc(1 / 0.625);
|
||||
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
--text-xxxs: 0.5625rem;
|
||||
--text-xxxs--line-height: calc(1 / 0.5625);
|
||||
--text-3xs: 0.5625rem;
|
||||
--text-3xs--line-height: calc(1 / 0.5625);
|
||||
|
||||
/* Font Families */
|
||||
--font-inter: 'Inter', sans-serif;
|
||||
|
||||
116
src/components/common/Badge.stories.ts
Normal file
116
src/components/common/Badge.stories.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Badge from './Badge.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Badges/Badge',
|
||||
component: Badge,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
label: { control: 'text' },
|
||||
severity: {
|
||||
control: 'select',
|
||||
options: ['default', 'secondary', 'warn', 'danger', 'contrast']
|
||||
},
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['label', 'dot', 'circle']
|
||||
}
|
||||
},
|
||||
args: {
|
||||
label: 'NEW',
|
||||
severity: 'default'
|
||||
}
|
||||
} satisfies Meta<typeof Badge>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
label: 'NEW',
|
||||
severity: 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
export const Warn: Story = {
|
||||
args: {
|
||||
label: 'NEW',
|
||||
severity: 'warn'
|
||||
}
|
||||
}
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
label: 'NEW',
|
||||
severity: 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
export const Contrast: Story = {
|
||||
args: {
|
||||
label: 'NEW',
|
||||
severity: 'contrast'
|
||||
}
|
||||
}
|
||||
|
||||
export const Circle: Story = {
|
||||
args: {
|
||||
label: '3',
|
||||
variant: 'circle'
|
||||
}
|
||||
}
|
||||
|
||||
export const AllSeveritiesLabel: Story = {
|
||||
render: () => ({
|
||||
components: { Badge },
|
||||
template: `
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge label="NEW" severity="default" />
|
||||
<Badge label="NEW" severity="secondary" />
|
||||
<Badge label="NEW" severity="warn" />
|
||||
<Badge label="NEW" severity="danger" />
|
||||
<Badge label="NEW" severity="contrast" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllSeveritiesDot: Story = {
|
||||
render: () => ({
|
||||
components: { Badge },
|
||||
template: `
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant="dot" severity="default" />
|
||||
<Badge variant="dot" severity="secondary" />
|
||||
<Badge variant="dot" severity="warn" />
|
||||
<Badge variant="dot" severity="danger" />
|
||||
<Badge variant="dot" severity="contrast" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => ({
|
||||
components: { Badge },
|
||||
template: `
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Badge label="NEW" variant="label" />
|
||||
<span class="text-xs text-muted-foreground">label</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Badge variant="dot" severity="danger" />
|
||||
<span class="text-xs text-muted-foreground">dot</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Badge label="5" variant="circle" />
|
||||
<span class="text-xs text-muted-foreground">circle</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
69
src/components/common/Badge.test.ts
Normal file
69
src/components/common/Badge.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import Badge from './Badge.vue'
|
||||
import { badgeVariants } from './badge.variants'
|
||||
|
||||
describe('Badge', () => {
|
||||
it('renders label text', () => {
|
||||
const wrapper = mount(Badge, { props: { label: 'NEW' } })
|
||||
expect(wrapper.text()).toBe('NEW')
|
||||
})
|
||||
|
||||
it('renders numeric label', () => {
|
||||
const wrapper = mount(Badge, { props: { label: 5 } })
|
||||
expect(wrapper.text()).toBe('5')
|
||||
})
|
||||
|
||||
it('defaults to dot variant when no label is provided', () => {
|
||||
const wrapper = mount(Badge)
|
||||
expect(wrapper.classes()).toContain('size-2')
|
||||
})
|
||||
|
||||
it('defaults to label variant when label is provided', () => {
|
||||
const wrapper = mount(Badge, { props: { label: 'NEW' } })
|
||||
expect(wrapper.classes()).toContain('font-semibold')
|
||||
expect(wrapper.classes()).toContain('uppercase')
|
||||
})
|
||||
|
||||
it('applies circle variant', () => {
|
||||
const wrapper = mount(Badge, {
|
||||
props: { label: '3', variant: 'circle' }
|
||||
})
|
||||
expect(wrapper.classes()).toContain('size-3.5')
|
||||
})
|
||||
|
||||
it('merges custom class via cn()', () => {
|
||||
const wrapper = mount(Badge, {
|
||||
props: { label: 'Test', class: 'ml-2' }
|
||||
})
|
||||
expect(wrapper.classes()).toContain('ml-2')
|
||||
expect(wrapper.classes()).toContain('rounded-full')
|
||||
})
|
||||
|
||||
describe('twMerge preserves color alongside text-3xs font size', () => {
|
||||
it.each([
|
||||
['default', 'text-white'],
|
||||
['secondary', 'text-white'],
|
||||
['warn', 'text-white'],
|
||||
['danger', 'text-white'],
|
||||
['contrast', 'text-base-background']
|
||||
] as const)(
|
||||
'%s severity retains its text color class',
|
||||
(severity, expectedColor) => {
|
||||
const classes = badgeVariants({ severity, variant: 'label' })
|
||||
expect(classes).toContain(expectedColor)
|
||||
expect(classes).toContain('text-3xs')
|
||||
}
|
||||
)
|
||||
|
||||
it('cn() does not clobber text-white when merging with text-3xs', () => {
|
||||
const wrapper = mount(Badge, {
|
||||
props: { label: 'Test', severity: 'danger' }
|
||||
})
|
||||
const classList = wrapper.classes()
|
||||
expect(classList).toContain('text-white')
|
||||
expect(classList).toContain('text-3xs')
|
||||
})
|
||||
})
|
||||
})
|
||||
36
src/components/common/Badge.vue
Normal file
36
src/components/common/Badge.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { badgeVariants } from './badge.variants'
|
||||
import type { BadgeVariants } from './badge.variants'
|
||||
|
||||
const {
|
||||
label,
|
||||
severity = 'default',
|
||||
variant,
|
||||
class: className
|
||||
} = defineProps<{
|
||||
label?: string | number
|
||||
severity?: BadgeVariants['severity']
|
||||
variant?: BadgeVariants['variant']
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const badgeClass = computed(() =>
|
||||
cn(
|
||||
badgeVariants({
|
||||
severity,
|
||||
variant: variant ?? (label == null ? 'dot' : 'label')
|
||||
}),
|
||||
className
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="badgeClass">
|
||||
{{ label }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -3,7 +3,7 @@
|
||||
data-testid="badge-pill"
|
||||
:class="
|
||||
cn(
|
||||
'flex items-center gap-1 rounded-sm border px-1.5 py-0.5 text-xxs',
|
||||
'flex items-center gap-1 rounded-sm border px-1.5 py-0.5 text-2xs',
|
||||
textColorClass
|
||||
)
|
||||
"
|
||||
|
||||
@@ -59,7 +59,7 @@ defineProps<{ itemClass: string; contentClass: string; item: MenuItem }>()
|
||||
<i v-if="item.checked" class="icon-[lucide--check] shrink-0" />
|
||||
<div
|
||||
v-else-if="item.new"
|
||||
class="flex shrink-0 items-center rounded-full bg-primary-background px-1 text-xxs leading-none font-bold"
|
||||
class="flex shrink-0 items-center rounded-full bg-primary-background px-1 text-2xs leading-none font-bold"
|
||||
v-text="t('contextMenu.new')"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import StatusBadge from './StatusBadge.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Common/StatusBadge',
|
||||
component: StatusBadge,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
label: { control: 'text' },
|
||||
severity: {
|
||||
control: 'select',
|
||||
options: ['default', 'secondary', 'warn', 'danger', 'contrast']
|
||||
},
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['label', 'dot', 'circle']
|
||||
}
|
||||
},
|
||||
args: {
|
||||
label: 'Status',
|
||||
severity: 'default'
|
||||
}
|
||||
} satisfies Meta<typeof StatusBadge>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Failed: Story = {
|
||||
args: {
|
||||
label: 'Failed',
|
||||
severity: 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
export const Finished: Story = {
|
||||
args: {
|
||||
label: 'Finished',
|
||||
severity: 'contrast'
|
||||
}
|
||||
}
|
||||
|
||||
export const Dot: Story = {
|
||||
args: {
|
||||
label: undefined,
|
||||
variant: 'dot',
|
||||
severity: 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
export const Circle: Story = {
|
||||
args: {
|
||||
label: '3',
|
||||
variant: 'circle'
|
||||
}
|
||||
}
|
||||
|
||||
export const AllSeverities: Story = {
|
||||
render: () => ({
|
||||
components: { StatusBadge },
|
||||
template: `
|
||||
<div class="flex items-center gap-2">
|
||||
<StatusBadge label="Default" severity="default" />
|
||||
<StatusBadge label="Secondary" severity="secondary" />
|
||||
<StatusBadge label="Warn" severity="warn" />
|
||||
<StatusBadge label="Danger" severity="danger" />
|
||||
<StatusBadge label="Contrast" severity="contrast" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => ({
|
||||
components: { StatusBadge },
|
||||
template: `
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<StatusBadge label="Label" variant="label" />
|
||||
<span class="text-xs text-muted">label</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<StatusBadge variant="dot" severity="danger" />
|
||||
<span class="text-xs text-muted">dot</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<StatusBadge label="5" variant="circle" />
|
||||
<span class="text-xs text-muted">circle</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
@@ -44,7 +44,7 @@ const {
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<span
|
||||
v-if="item.badge"
|
||||
class="ml-3 flex items-center gap-1 rounded-full bg-(--primary-background) px-1.5 py-0.5 text-xxs text-base-foreground uppercase"
|
||||
class="ml-3 flex items-center gap-1 rounded-full bg-(--primary-background) px-1.5 py-0.5 text-2xs text-base-foreground uppercase"
|
||||
>
|
||||
{{ item.badge }}
|
||||
</span>
|
||||
|
||||
26
src/components/common/badge.variants.ts
Normal file
26
src/components/common/badge.variants.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { VariantProps } from 'cva'
|
||||
import { cva } from 'cva'
|
||||
|
||||
export const badgeVariants = cva({
|
||||
base: 'inline-flex items-center justify-center rounded-full',
|
||||
variants: {
|
||||
severity: {
|
||||
default: 'bg-primary-background text-white',
|
||||
secondary: 'bg-secondary-background-hover text-white',
|
||||
warn: 'bg-warning-background text-white',
|
||||
danger: 'bg-destructive-background text-white',
|
||||
contrast: 'bg-base-foreground text-base-background'
|
||||
},
|
||||
variant: {
|
||||
label: 'h-3.5 px-1 text-3xs font-semibold uppercase',
|
||||
dot: 'size-2',
|
||||
circle: 'size-3.5 text-3xs font-semibold'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
severity: 'default',
|
||||
variant: 'label'
|
||||
}
|
||||
})
|
||||
|
||||
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||
@@ -12,9 +12,9 @@ export const statusBadgeVariants = cva({
|
||||
contrast: 'bg-base-foreground text-base-background'
|
||||
},
|
||||
variant: {
|
||||
label: 'h-3.5 px-1 text-xxxs font-semibold uppercase',
|
||||
label: 'h-3.5 px-1 text-3xs font-semibold uppercase',
|
||||
dot: 'size-2',
|
||||
circle: 'size-3.5 text-xxxs font-semibold'
|
||||
circle: 'size-3.5 text-3xs font-semibold'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -49,14 +49,14 @@
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<h4
|
||||
class="m-0 text-xxs font-semibold tracking-wide text-muted-foreground uppercase"
|
||||
class="m-0 text-2xs font-semibold tracking-wide text-muted-foreground uppercase"
|
||||
>
|
||||
{{ $t('nodeHelpPage.inputs') }}
|
||||
</h4>
|
||||
<div
|
||||
v-for="input in inputs"
|
||||
:key="input.name"
|
||||
class="flex items-center justify-between gap-2 text-xxs"
|
||||
class="flex items-center justify-between gap-2 text-2xs"
|
||||
>
|
||||
<span class="text-foreground shrink-0">{{ input.name }}</span>
|
||||
<span class="min-w-0 truncate text-muted-foreground">{{
|
||||
@@ -71,14 +71,14 @@
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<h4
|
||||
class="m-0 text-xxs font-semibold tracking-wide text-muted-foreground uppercase"
|
||||
class="m-0 text-2xs font-semibold tracking-wide text-muted-foreground uppercase"
|
||||
>
|
||||
{{ $t('nodeHelpPage.outputs') }}
|
||||
</h4>
|
||||
<div
|
||||
v-for="output in outputs"
|
||||
:key="output.name"
|
||||
class="flex items-center justify-between gap-2 text-xxs"
|
||||
class="flex items-center justify-between gap-2 text-2xs"
|
||||
>
|
||||
<span class="text-foreground shrink-0">{{ output.name }}</span>
|
||||
<span class="min-w-0 truncate text-muted-foreground">{{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<template #alt-title>
|
||||
<span
|
||||
class="ml-2 flex items-center rounded-full bg-primary-background px-1.5 py-0.5 text-xxs text-base-foreground uppercase"
|
||||
class="ml-2 flex items-center rounded-full bg-primary-background px-1.5 py-0.5 text-2xs text-base-foreground uppercase"
|
||||
>
|
||||
{{ $t('g.beta') }}
|
||||
</span>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
/>
|
||||
<div
|
||||
v-else-if="badge.label"
|
||||
class="shrink-0 rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
|
||||
class="shrink-0 rounded-full px-1.5 py-0.5 text-3xs font-semibold"
|
||||
:class="labelClasses"
|
||||
>
|
||||
{{ badge.label }}
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="flex max-w-xs min-w-40 flex-col gap-2 p-3">
|
||||
<div
|
||||
v-if="badge.label"
|
||||
class="w-fit rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
|
||||
class="w-fit rounded-full px-1.5 py-0.5 text-3xs font-semibold"
|
||||
:class="labelClasses"
|
||||
>
|
||||
{{ badge.label }}
|
||||
@@ -68,7 +68,7 @@
|
||||
/>
|
||||
<div
|
||||
v-if="badge.label"
|
||||
class="shrink-0 rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
|
||||
class="shrink-0 rounded-full px-1.5 py-0.5 text-3xs font-semibold"
|
||||
:class="labelClasses"
|
||||
>
|
||||
{{ badge.label }}
|
||||
@@ -87,7 +87,7 @@
|
||||
<div class="flex max-w-xs min-w-40 flex-col gap-2 p-3">
|
||||
<div
|
||||
v-if="badge.label"
|
||||
class="w-fit rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
|
||||
class="w-fit rounded-full px-1.5 py-0.5 text-3xs font-semibold"
|
||||
:class="labelClasses"
|
||||
>
|
||||
{{ badge.label }}
|
||||
@@ -115,7 +115,7 @@
|
||||
/>
|
||||
<div
|
||||
v-if="badge.label"
|
||||
class="shrink-0 rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
|
||||
class="shrink-0 rounded-full px-1.5 py-0.5 text-3xs font-semibold"
|
||||
:class="labelClasses"
|
||||
>
|
||||
{{ badge.label }}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const WithLabel: Story = {
|
||||
},
|
||||
template: `
|
||||
<div class="relative max-w-sm rounded-lg bg-component-node-widget-background">
|
||||
<label class="pointer-events-none absolute left-3 top-1.5 text-xxs text-muted-foreground z-10">
|
||||
<label class="pointer-events-none absolute left-3 top-1.5 text-2xs text-muted-foreground z-10">
|
||||
Prompt
|
||||
</label>
|
||||
<Textarea
|
||||
|
||||
@@ -560,7 +560,7 @@ function drawDisconnectedPlaceholder(
|
||||
'#333'
|
||||
)
|
||||
const textColor = readDesignToken('--color-text-secondary', '#999')
|
||||
const fontSize = readDesignToken('--text-xxs', '11px')
|
||||
const fontSize = readDesignToken('--text-2xs', '11px')
|
||||
const fontFamily = readDesignToken('--font-inter', 'sans-serif')
|
||||
|
||||
ctx.save()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex items-center gap-2 p-4 font-bold">
|
||||
<span>{{ $t('assetBrowser.uploadModelGeneric') }}</span>
|
||||
<span
|
||||
class="rounded-full bg-white px-1.5 py-0 font-inter text-xxs font-semibold text-black uppercase"
|
||||
class="rounded-full bg-white px-1.5 py-0 font-inter text-2xs font-semibold text-black uppercase"
|
||||
>
|
||||
{{ $t('g.beta') }}
|
||||
</span>
|
||||
|
||||
@@ -41,7 +41,7 @@ const templateSelectorDialog = useWorkflowTemplateSelectorDialog()
|
||||
<i18n-t keypath="linearMode.welcome.getStarted" tag="span">
|
||||
<template #runButton>
|
||||
<span
|
||||
class="mx-0.5 inline-flex -translate-y-0.5 transform cursor-default items-center rounded-sm bg-primary-background px-3.5 py-0.5 text-xxs font-medium text-base-foreground"
|
||||
class="mx-0.5 inline-flex -translate-y-0.5 transform cursor-default items-center rounded-sm bg-primary-background px-3.5 py-0.5 text-2xs font-medium text-base-foreground"
|
||||
>
|
||||
{{ t('menu.run') }}
|
||||
</span>
|
||||
@@ -86,7 +86,7 @@ const templateSelectorDialog = useWorkflowTemplateSelectorDialog()
|
||||
<i class="icon-[lucide--hammer]" />
|
||||
{{ t('linearMode.welcome.buildApp') }}
|
||||
<div
|
||||
class="absolute -top-2 -right-2 rounded-full bg-base-foreground px-1 text-xxs text-base-background"
|
||||
class="absolute -top-2 -right-2 rounded-full bg-base-foreground px-1 text-2xs text-base-background"
|
||||
>
|
||||
{{ t('g.experimental') }}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<label
|
||||
v-if="!hideLayoutField"
|
||||
:for="id"
|
||||
class="pointer-events-none absolute top-1.5 left-3 z-10 text-xxs text-muted-foreground"
|
||||
class="pointer-events-none absolute top-1.5 left-3 z-10 text-2xs text-muted-foreground"
|
||||
>
|
||||
{{ displayName }}
|
||||
</label>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
class="pi pi-arrow-circle-up text-xs text-blue-600"
|
||||
/>
|
||||
<span>{{ installedVersion }}</span>
|
||||
<i v-if="!isDisabled" class="pi pi-chevron-right text-xxs" />
|
||||
<i v-if="!isDisabled" class="pi pi-chevron-right text-2xs" />
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
|
||||
Reference in New Issue
Block a user