Skip to content
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
120 changes: 120 additions & 0 deletions src/components/NeBadgeV2.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<!--
Copyright (C) 2025 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script lang="ts" setup>
import { computed } from 'vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faXmark } from '@fortawesome/free-solid-svg-icons'

const {
size = 'sm',
kind = 'gray',
pill = true,
dismissable = false,
customKindClasses = '',
dismissAriaLabel = 'Dismiss'
} = defineProps<{
size?: 'xs' | 'sm'
kind?: 'primary' | 'indigo' | 'gray' | 'green' | 'amber' | 'rose' | 'blue' | 'custom'
pill?: boolean
dismissable?: boolean
customKindClasses?: string
dismissAriaLabel?: string
}>()

const emit = defineEmits(['dismiss'])

const textClasses = computed(() => {
switch (size) {
case 'xs':
return 'text-xs'
case 'sm':
default:
return 'text-sm'
}
})

const paddingClasses = computed(() => {
switch (size) {
case 'xs':
return 'px-3'
case 'sm':
default:
return 'px-2.5'
}
})

const spacingClasses = computed(() => {
return 'gap-x-1'
})

const kindClasses = computed(() => {
switch (kind) {
case 'primary':
return 'bg-primary-100 text-primary-800 dark:bg-primary-700 dark:text-primary-100'
case 'indigo':
return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-700 dark:text-indigo-100'
case 'green':
return 'bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-50'
case 'amber':
return 'bg-amber-100 text-amber-800 dark:bg-amber-700 dark:text-amber-50'
case 'rose':
return 'bg-rose-100 text-rose-800 dark:bg-rose-700 dark:text-rose-100'
case 'blue':
return 'bg-blue-100 text-blue-800 dark:bg-blue-700 dark:text-blue-100'
case 'custom':
return `${customKindClasses}`
case 'gray':
default:
return 'bg-gray-200 text-gray-800 dark:bg-gray-600 dark:text-gray-100'
}
})

const dismissButtonClasses = computed(() => {
switch (kind) {
case 'primary':
return 'hover:bg-primary-200 hover:dark:bg-primary-600'
case 'indigo':
return 'hover:bg-indigo-200 hover:dark:bg-indigo-500'
case 'green':
return 'hover:bg-green-200 hover:dark:bg-green-600'
case 'amber':
return 'hover:bg-amber-200 hover:dark:bg-amber-600'
case 'rose':
return 'hover:bg-rose-200 hover:dark:bg-rose-600'
case 'blue':
return 'hover:bg-blue-200 hover:dark:bg-blue-600'
case 'custom':
return 'hover:bg-white/20'
case 'gray':
default:
return 'hover:bg-gray-300 hover:dark:bg-gray-500'
}
})
</script>

<template>
<div
:class="[
textClasses,
spacingClasses,
paddingClasses,
kindClasses,
pill ? 'rounded-full' : 'rounded'
]"
class="inline-flex w-fit items-center py-0.5 font-medium"
>
<slot />
<button
v-if="dismissable"
:class="`inline-flex rounded focus:ring-2 focus:ring-offset-2 focus:outline-hidden ${dismissButtonClasses}`"
type="button"
@click="emit('dismiss')"
>
<span class="sr-only">{{ dismissAriaLabel }}</span>
<FontAwesomeIcon :icon="faXmark" class="size-4" aria-hidden="true" />
</button>
</div>
</template>
3 changes: 3 additions & 0 deletions src/components/NeDropdownFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ export interface Props {
size?: ButtonSize
disabled?: boolean
id?: string
clearSearchLabel: string
}

const props = withDefaults(defineProps<Props>(), {
showClearFilter: true,
showSelectionCount: true,
showOptionsFilter: false,
clearSearchLabel: 'Clear',
optionsFilterPlaceholder: '',
maxOptionsShown: 25,
alignToRight: false,
Expand Down Expand Up @@ -220,6 +222,7 @@ function maybeFocusOptionsFilter() {
ref="optionsFilterRef"
v-model="optionsFilter"
:placeholder="optionsFilterPlaceholder"
:clear-search-label="clearSearchLabel"
is-search
@keydown.stop
/>
Expand Down
4 changes: 3 additions & 1 deletion src/components/NeListbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
id: string
label: string
description?: string
rawObj?: any

Check warning on line 26 in src/components/NeListbox.vue

View workflow job for this annotation

GitHub Actions / Eslint

Unexpected any. Specify a different type
disabled?: boolean
}

Expand Down Expand Up @@ -64,11 +64,12 @@
})

const query = ref('')
const selected = ref(props.multiple ? [] : null) as any

Check warning on line 67 in src/components/NeListbox.vue

View workflow job for this annotation

GitHub Actions / Eslint

Unexpected any. Specify a different type
const showOptions = ref(false)
const listboxRef = ref<HTMLDivElement | null>(null)
const top = ref(0)
const left = ref(0)
const width = ref(0)
const buttonRef = ref<InstanceType<typeof Listbox> | null>(null)

const inputValidStyle =
Expand Down Expand Up @@ -137,6 +138,7 @@
function calculatePosition() {
top.value = buttonRef.value?.$el.getBoundingClientRect().bottom + window.scrollY
left.value = buttonRef.value?.$el.getBoundingClientRect().left - window.scrollX
width.value = buttonRef.value?.$el.getBoundingClientRect().width || 0
}

function onClickOutsideListbox() {
Expand Down Expand Up @@ -246,7 +248,7 @@
<ListboxOptions
static
: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}`"
:style="[{ top: top + 'px' }, { left: left + 'px' }]"
:style="[{ top: top + 'px' }, { left: left + 'px' }, { 'min-width': width + 'px' }]"
>
<ListboxOption
v-for="option in allOptions"
Expand Down
13 changes: 11 additions & 2 deletions src/components/NeSideDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faXmark } from '@fortawesome/free-solid-svg-icons'
import { onMounted, onUnmounted } from 'vue'
import { onMounted, onUnmounted, watch } from 'vue'

const props = defineProps({
isShown: { type: Boolean, default: false },
Expand All @@ -16,7 +16,16 @@
closeAriaLabel: { type: String, default: 'Close side drawer' }
})

const emit = defineEmits(['close'])
const emit = defineEmits(['close', 'show'])

watch(
() => props.isShown,
() => {
if (props.isShown) {
emit('show')
}
}
)

onMounted(() => {
window.addEventListener('keydown', onKeyUp)
Expand All @@ -37,7 +46,7 @@
}
}

function onKeyUp(event: any) {

Check warning on line 49 in src/components/NeSideDrawer.vue

View workflow job for this annotation

GitHub Actions / Eslint

Unexpected any. Specify a different type
// close drawer on escape key
if (event.key === 'Escape') {
closeDrawer()
Expand Down
2 changes: 1 addition & 1 deletion src/components/NeTableRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const trCardStyle: Record<Breakpoint, string> = {
}
</script>
<template>
<tr :class="[`grid`, trCardStyle[cardBreakpoint]]">
<tr :class="[`grid`, `hover:bg-gray-100 dark:hover:bg-gray-800`, trCardStyle[cardBreakpoint]]">
<slot />
</tr>
</template>
4 changes: 3 additions & 1 deletion src/components/NeToastNotification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ library.add(fasXmark)
class="text-gray-500 dark:text-gray-400"
>
<template #trigger>
{{ humanDistanceToNowLoc(notification.timestamp) }}
<span class="cursor-pointer">
{{ humanDistanceToNowLoc(notification.timestamp) }}
</span>
</template>
<template #content>
{{ formatDateLoc(notification.timestamp, 'Pp') }}
Expand Down
8 changes: 4 additions & 4 deletions src/components/NeTooltip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ defineProps({

<template>
<Tippy :interactive="interactive" :placement="placement" :trigger="triggerEvent" theme="tailwind">
<button type="button" class="inline-flex">
<slot name="trigger">
<slot name="trigger">
<button type="button" class="inline-flex">
<FontAwesomeIcon
:icon="faCircleInfo"
class="h-4 w-4 text-indigo-500 dark:text-indigo-300"
/>
</slot>
</button>
</button>
</slot>
<template #content>
<slot name="content" />
</template>
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export { default as NeListbox } from './components/NeListbox.vue'
export { default as NeDropdownFilter } from './components/NeDropdownFilter.vue'
export { default as NeSortDropdown } from './components/NeSortDropdown.vue'
export { default as NeAvatar } from './components/NeAvatar.vue'
export { default as NeBadgeV2 } from './components/NeBadgeV2.vue'

// types export
export type { NeComboboxOption } from './components/NeCombobox.vue'
Expand Down
112 changes: 112 additions & 0 deletions stories/NeBadgeV2.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (C) 2025 Nethesis S.r.l.
// SPDX-License-Identifier: GPL-3.0-or-later

import { Meta, StoryObj } from '@storybook/vue3-vite'
import { NeBadgeV2 } from '../src/main'
import { faAward } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'

const meta: Meta<typeof NeBadgeV2> = {
title: 'NeBadgeV2',
component: NeBadgeV2,
tags: ['autodocs'],
argTypes: {
size: {
options: ['xs', 'sm'],
control: { type: 'inline-radio' }
},
kind: {
options: ['primary', 'gray', 'indigo', 'green', 'amber', 'rose', 'blue', 'custom'],
control: { type: 'inline-radio' }
}
},
args: {
size: 'sm',
kind: 'gray',
pill: true,
dismissable: false,
customKindClasses: '',
dismissAriaLabel: 'Dismiss'
}
}

export default meta
type Story = StoryObj<typeof meta>

const defaultTemplate = `<NeBadgeV2 v-bind="args">Badge</NeBadgeV2>`

export const Default: Story = {
render: (args) => ({
components: { NeBadgeV2 },
setup() {
return { args }
},
template: defaultTemplate
}),
args: {}
}

export const Kind: Story = {
render: (args) => ({
components: { NeBadgeV2 },
setup() {
return { args }
},
template: `
<div class="flex flex-wrap gap-8">
<NeBadgeV2 v-bind="args" kind="gray">gray</NeBadgeV2>
<NeBadgeV2 v-bind="args" kind="primary">primary</NeBadgeV2>
<NeBadgeV2 v-bind="args" kind="indigo">indigo</NeBadgeV2>
<NeBadgeV2 v-bind="args" kind="green">green</NeBadgeV2>
<NeBadgeV2 v-bind="args" kind="amber">amber</NeBadgeV2>
<NeBadgeV2 v-bind="args" kind="rose">rose</NeBadgeV2>
<NeBadgeV2 v-bind="args" kind="blue">blue</NeBadgeV2>
<NeBadgeV2 v-bind="args" kind="custom" customKindClasses="bg-fuchsia-100 text-fuchsia-700 dark:bg-fuchsia-700 dark:text-fuchsia-50">custom</NeBadgeV2>
</div>
`
}),
args: {}
}

export const CustomKind: Story = {
render: (args) => ({
components: { NeBadgeV2 },
setup() {
return { args }
},
template: `<NeBadgeV2 v-bind="args">Custom badge</NeBadgeV2>`
}),
args: {
kind: 'custom',
customKindClasses: 'text-white bg-linear-to-br from-fuchsia-500 to-blue-500'
}
}

const withIconTemplate = `<NeBadgeV2 v-bind="args">
<FontAwesomeIcon :icon="faAward" class="size-4" />
Badge
</NeBadgeV2>`

export const WithIcon: Story = {
render: (args) => ({
components: { NeBadgeV2, FontAwesomeIcon },
setup() {
return { args, faAward }
},
template: withIconTemplate
}),
args: {}
}

export const Dismissable: Story = {
render: (args) => ({
components: { NeBadgeV2 },
setup() {
return { args }
},
template: defaultTemplate
}),
args: {
dismissable: true
}
}
20 changes: 20 additions & 0 deletions stories/NeButton.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

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

const meta = {
title: 'NeButton',
Expand Down Expand Up @@ -133,6 +135,24 @@ export const WithSuffix: Story = {
args: { loadingPosition: 'suffix' }
}

const templateIconOnly = `<div>
<NeButton v-bind="args">
<FontAwesomeIcon :icon="faCopy" class="h-6 w-4" aria-hidden="true" />
</NeButton>
<div class="mt-4">It is recommended to show a tooltip when the cursor hovers over the button.</div>
</div>`

export const IconOnly: Story = {
render: (args) => ({
components: { NeButton, FontAwesomeIcon },
setup() {
return { args, faCopy }
},
template: templateIconOnly
}),
args: {}
}

export const Disabled: Story = {
render: (args) => ({
components: { NeButton },
Expand Down
1 change: 1 addition & 0 deletions stories/NeDropdownFilter.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const meta = {
showSelectionCount: true,
noOptionsLabel: 'No options',
showOptionsFilter: false,
clearSearchLabel: 'Clear',
optionsFilterPlaceholder: 'Filter options',
maxOptionsShown: 25,
moreOptionsHiddenLabel: 'Continue typing to show more options',
Expand Down
Loading