Skip to content

Commit 466de89

Browse files
authored
feat: add NeBadgeV2 (#93)
fix(NeButton): add story for icon only button fix(NeTooltip): prevent nesting of button elements feat(NeTable): add hover color to table rows fix(NeDropdownFilter): add clearSearchLabel prop fix(NeSideDrawer): add show event fix(NeListbox): fix width of options list
1 parent fdae982 commit 466de89

File tree

11 files changed

+279
-9
lines changed

11 files changed

+279
-9
lines changed

src/components/NeBadgeV2.vue

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<!--
2+
Copyright (C) 2025 Nethesis S.r.l.
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
-->
5+
6+
<script lang="ts" setup>
7+
import { computed } from 'vue'
8+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
9+
import { faXmark } from '@fortawesome/free-solid-svg-icons'
10+
11+
const {
12+
size = 'sm',
13+
kind = 'gray',
14+
pill = true,
15+
dismissable = false,
16+
customKindClasses = '',
17+
dismissAriaLabel = 'Dismiss'
18+
} = defineProps<{
19+
size?: 'xs' | 'sm'
20+
kind?: 'primary' | 'indigo' | 'gray' | 'green' | 'amber' | 'rose' | 'blue' | 'custom'
21+
pill?: boolean
22+
dismissable?: boolean
23+
customKindClasses?: string
24+
dismissAriaLabel?: string
25+
}>()
26+
27+
const emit = defineEmits(['dismiss'])
28+
29+
const textClasses = computed(() => {
30+
switch (size) {
31+
case 'xs':
32+
return 'text-xs'
33+
case 'sm':
34+
default:
35+
return 'text-sm'
36+
}
37+
})
38+
39+
const paddingClasses = computed(() => {
40+
switch (size) {
41+
case 'xs':
42+
return 'px-3'
43+
case 'sm':
44+
default:
45+
return 'px-2.5'
46+
}
47+
})
48+
49+
const spacingClasses = computed(() => {
50+
return 'gap-x-1'
51+
})
52+
53+
const kindClasses = computed(() => {
54+
switch (kind) {
55+
case 'primary':
56+
return 'bg-primary-100 text-primary-800 dark:bg-primary-700 dark:text-primary-100'
57+
case 'indigo':
58+
return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-700 dark:text-indigo-100'
59+
case 'green':
60+
return 'bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-50'
61+
case 'amber':
62+
return 'bg-amber-100 text-amber-800 dark:bg-amber-700 dark:text-amber-50'
63+
case 'rose':
64+
return 'bg-rose-100 text-rose-800 dark:bg-rose-700 dark:text-rose-100'
65+
case 'blue':
66+
return 'bg-blue-100 text-blue-800 dark:bg-blue-700 dark:text-blue-100'
67+
case 'custom':
68+
return `${customKindClasses}`
69+
case 'gray':
70+
default:
71+
return 'bg-gray-200 text-gray-800 dark:bg-gray-600 dark:text-gray-100'
72+
}
73+
})
74+
75+
const dismissButtonClasses = computed(() => {
76+
switch (kind) {
77+
case 'primary':
78+
return 'hover:bg-primary-200 hover:dark:bg-primary-600'
79+
case 'indigo':
80+
return 'hover:bg-indigo-200 hover:dark:bg-indigo-500'
81+
case 'green':
82+
return 'hover:bg-green-200 hover:dark:bg-green-600'
83+
case 'amber':
84+
return 'hover:bg-amber-200 hover:dark:bg-amber-600'
85+
case 'rose':
86+
return 'hover:bg-rose-200 hover:dark:bg-rose-600'
87+
case 'blue':
88+
return 'hover:bg-blue-200 hover:dark:bg-blue-600'
89+
case 'custom':
90+
return 'hover:bg-white/20'
91+
case 'gray':
92+
default:
93+
return 'hover:bg-gray-300 hover:dark:bg-gray-500'
94+
}
95+
})
96+
</script>
97+
98+
<template>
99+
<div
100+
:class="[
101+
textClasses,
102+
spacingClasses,
103+
paddingClasses,
104+
kindClasses,
105+
pill ? 'rounded-full' : 'rounded'
106+
]"
107+
class="inline-flex w-fit items-center py-0.5 font-medium"
108+
>
109+
<slot />
110+
<button
111+
v-if="dismissable"
112+
:class="`inline-flex rounded focus:ring-2 focus:ring-offset-2 focus:outline-hidden ${dismissButtonClasses}`"
113+
type="button"
114+
@click="emit('dismiss')"
115+
>
116+
<span class="sr-only">{{ dismissAriaLabel }}</span>
117+
<FontAwesomeIcon :icon="faXmark" class="size-4" aria-hidden="true" />
118+
</button>
119+
</div>
120+
</template>

src/components/NeDropdownFilter.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ export interface Props {
5151
size?: ButtonSize
5252
disabled?: boolean
5353
id?: string
54+
clearSearchLabel: string
5455
}
5556
5657
const props = withDefaults(defineProps<Props>(), {
5758
showClearFilter: true,
5859
showSelectionCount: true,
5960
showOptionsFilter: false,
61+
clearSearchLabel: 'Clear',
6062
optionsFilterPlaceholder: '',
6163
maxOptionsShown: 25,
6264
alignToRight: false,
@@ -220,6 +222,7 @@ function maybeFocusOptionsFilter() {
220222
ref="optionsFilterRef"
221223
v-model="optionsFilter"
222224
:placeholder="optionsFilterPlaceholder"
225+
:clear-search-label="clearSearchLabel"
223226
is-search
224227
@keydown.stop
225228
/>

src/components/NeListbox.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const showOptions = ref(false)
6969
const listboxRef = ref<HTMLDivElement | null>(null)
7070
const top = ref(0)
7171
const left = ref(0)
72+
const width = ref(0)
7273
const buttonRef = ref<InstanceType<typeof Listbox> | null>(null)
7374
7475
const inputValidStyle =
@@ -137,6 +138,7 @@ watch(
137138
function calculatePosition() {
138139
top.value = buttonRef.value?.$el.getBoundingClientRect().bottom + window.scrollY
139140
left.value = buttonRef.value?.$el.getBoundingClientRect().left - window.scrollX
141+
width.value = buttonRef.value?.$el.getBoundingClientRect().width || 0
140142
}
141143
142144
function onClickOutsideListbox() {
@@ -246,7 +248,7 @@ onClickOutside(listboxRef, () => onClickOutsideListbox())
246248
<ListboxOptions
247249
static
248250
:class="`absolute z-10 mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-gray-500/5 focus:outline-hidden sm:text-sm dark:bg-gray-950 ${optionsPanelStyle}`"
249-
:style="[{ top: top + 'px' }, { left: left + 'px' }]"
251+
:style="[{ top: top + 'px' }, { left: left + 'px' }, { 'min-width': width + 'px' }]"
250252
>
251253
<ListboxOption
252254
v-for="option in allOptions"

src/components/NeSideDrawer.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
88
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
99
import { faXmark } from '@fortawesome/free-solid-svg-icons'
10-
import { onMounted, onUnmounted } from 'vue'
10+
import { onMounted, onUnmounted, watch } from 'vue'
1111
1212
const props = defineProps({
1313
isShown: { type: Boolean, default: false },
@@ -16,7 +16,16 @@ const props = defineProps({
1616
closeAriaLabel: { type: String, default: 'Close side drawer' }
1717
})
1818
19-
const emit = defineEmits(['close'])
19+
const emit = defineEmits(['close', 'show'])
20+
21+
watch(
22+
() => props.isShown,
23+
() => {
24+
if (props.isShown) {
25+
emit('show')
26+
}
27+
}
28+
)
2029
2130
onMounted(() => {
2231
window.addEventListener('keydown', onKeyUp)

src/components/NeTableRow.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const trCardStyle: Record<Breakpoint, string> = {
1818
}
1919
</script>
2020
<template>
21-
<tr :class="[`grid`, trCardStyle[cardBreakpoint]]">
21+
<tr :class="[`grid`, `hover:bg-gray-100 dark:hover:bg-gray-800`, trCardStyle[cardBreakpoint]]">
2222
<slot />
2323
</tr>
2424
</template>

src/components/NeToastNotification.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ library.add(fasXmark)
8989
class="text-gray-500 dark:text-gray-400"
9090
>
9191
<template #trigger>
92-
{{ humanDistanceToNowLoc(notification.timestamp) }}
92+
<span class="cursor-pointer">
93+
{{ humanDistanceToNowLoc(notification.timestamp) }}
94+
</span>
9395
</template>
9496
<template #content>
9597
{{ formatDateLoc(notification.timestamp, 'Pp') }}

src/components/NeTooltip.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ defineProps({
5151

5252
<template>
5353
<Tippy :interactive="interactive" :placement="placement" :trigger="triggerEvent" theme="tailwind">
54-
<button type="button" class="inline-flex">
55-
<slot name="trigger">
54+
<slot name="trigger">
55+
<button type="button" class="inline-flex">
5656
<FontAwesomeIcon
5757
:icon="faCircleInfo"
5858
class="h-4 w-4 text-indigo-500 dark:text-indigo-300"
5959
/>
60-
</slot>
61-
</button>
60+
</button>
61+
</slot>
6262
<template #content>
6363
<slot name="content" />
6464
</template>

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export { default as NeListbox } from './components/NeListbox.vue'
3939
export { default as NeDropdownFilter } from './components/NeDropdownFilter.vue'
4040
export { default as NeSortDropdown } from './components/NeSortDropdown.vue'
4141
export { default as NeAvatar } from './components/NeAvatar.vue'
42+
export { default as NeBadgeV2 } from './components/NeBadgeV2.vue'
4243

4344
// types export
4445
export type { NeComboboxOption } from './components/NeCombobox.vue'

stories/NeBadgeV2.stories.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (C) 2025 Nethesis S.r.l.
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
import { Meta, StoryObj } from '@storybook/vue3-vite'
5+
import { NeBadgeV2 } from '../src/main'
6+
import { faAward } from '@fortawesome/free-solid-svg-icons'
7+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
8+
9+
const meta: Meta<typeof NeBadgeV2> = {
10+
title: 'NeBadgeV2',
11+
component: NeBadgeV2,
12+
tags: ['autodocs'],
13+
argTypes: {
14+
size: {
15+
options: ['xs', 'sm'],
16+
control: { type: 'inline-radio' }
17+
},
18+
kind: {
19+
options: ['primary', 'gray', 'indigo', 'green', 'amber', 'rose', 'blue', 'custom'],
20+
control: { type: 'inline-radio' }
21+
}
22+
},
23+
args: {
24+
size: 'sm',
25+
kind: 'gray',
26+
pill: true,
27+
dismissable: false,
28+
customKindClasses: '',
29+
dismissAriaLabel: 'Dismiss'
30+
}
31+
}
32+
33+
export default meta
34+
type Story = StoryObj<typeof meta>
35+
36+
const defaultTemplate = `<NeBadgeV2 v-bind="args">Badge</NeBadgeV2>`
37+
38+
export const Default: Story = {
39+
render: (args) => ({
40+
components: { NeBadgeV2 },
41+
setup() {
42+
return { args }
43+
},
44+
template: defaultTemplate
45+
}),
46+
args: {}
47+
}
48+
49+
export const Kind: Story = {
50+
render: (args) => ({
51+
components: { NeBadgeV2 },
52+
setup() {
53+
return { args }
54+
},
55+
template: `
56+
<div class="flex flex-wrap gap-8">
57+
<NeBadgeV2 v-bind="args" kind="gray">gray</NeBadgeV2>
58+
<NeBadgeV2 v-bind="args" kind="primary">primary</NeBadgeV2>
59+
<NeBadgeV2 v-bind="args" kind="indigo">indigo</NeBadgeV2>
60+
<NeBadgeV2 v-bind="args" kind="green">green</NeBadgeV2>
61+
<NeBadgeV2 v-bind="args" kind="amber">amber</NeBadgeV2>
62+
<NeBadgeV2 v-bind="args" kind="rose">rose</NeBadgeV2>
63+
<NeBadgeV2 v-bind="args" kind="blue">blue</NeBadgeV2>
64+
<NeBadgeV2 v-bind="args" kind="custom" customKindClasses="bg-fuchsia-100 text-fuchsia-700 dark:bg-fuchsia-700 dark:text-fuchsia-50">custom</NeBadgeV2>
65+
</div>
66+
`
67+
}),
68+
args: {}
69+
}
70+
71+
export const CustomKind: Story = {
72+
render: (args) => ({
73+
components: { NeBadgeV2 },
74+
setup() {
75+
return { args }
76+
},
77+
template: `<NeBadgeV2 v-bind="args">Custom badge</NeBadgeV2>`
78+
}),
79+
args: {
80+
kind: 'custom',
81+
customKindClasses: 'text-white bg-linear-to-br from-fuchsia-500 to-blue-500'
82+
}
83+
}
84+
85+
const withIconTemplate = `<NeBadgeV2 v-bind="args">
86+
<FontAwesomeIcon :icon="faAward" class="size-4" />
87+
Badge
88+
</NeBadgeV2>`
89+
90+
export const WithIcon: Story = {
91+
render: (args) => ({
92+
components: { NeBadgeV2, FontAwesomeIcon },
93+
setup() {
94+
return { args, faAward }
95+
},
96+
template: withIconTemplate
97+
}),
98+
args: {}
99+
}
100+
101+
export const Dismissable: Story = {
102+
render: (args) => ({
103+
components: { NeBadgeV2 },
104+
setup() {
105+
return { args }
106+
},
107+
template: defaultTemplate
108+
}),
109+
args: {
110+
dismissable: true
111+
}
112+
}

stories/NeButton.stories.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import type { Meta, StoryObj } from '@storybook/vue3-vite'
55
import { NeButton } from '../src/main'
6+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
7+
import { faCopy } from '@fortawesome/free-solid-svg-icons'
68

79
const meta = {
810
title: 'NeButton',
@@ -133,6 +135,24 @@ export const WithSuffix: Story = {
133135
args: { loadingPosition: 'suffix' }
134136
}
135137

138+
const templateIconOnly = `<div>
139+
<NeButton v-bind="args">
140+
<FontAwesomeIcon :icon="faCopy" class="h-6 w-4" aria-hidden="true" />
141+
</NeButton>
142+
<div class="mt-4">It is recommended to show a tooltip when the cursor hovers over the button.</div>
143+
</div>`
144+
145+
export const IconOnly: Story = {
146+
render: (args) => ({
147+
components: { NeButton, FontAwesomeIcon },
148+
setup() {
149+
return { args, faCopy }
150+
},
151+
template: templateIconOnly
152+
}),
153+
args: {}
154+
}
155+
136156
export const Disabled: Story = {
137157
render: (args) => ({
138158
components: { NeButton },

0 commit comments

Comments
 (0)