Skip to content

Commit 661c682

Browse files
authored
feat: add NeCard and NeDropdown #12
Add NeDropdown and NeCard components
2 parents b3f294f + 8731287 commit 661c682

File tree

7 files changed

+604
-0
lines changed

7 files changed

+604
-0
lines changed

.storybook/preview.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Preview, VueRenderer } from '@storybook/vue3'
22
import { withThemeByClassName } from '@storybook/addon-themes'
33

44
import '../src/main.css'
5+
import './storybook.css'
56

67
const preview: Preview = {
78
parameters: {

.storybook/storybook.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.sb-show-main.sb-main-padded {
2+
/* remove default padding from component area */
3+
padding: 0;
4+
}
5+
6+
#storybook-root {
7+
/* set background color according to theme */
8+
@apply h-screen bg-gray-50 dark:bg-gray-900;
9+
}

src/components/NeCard.vue

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<!--
2+
Copyright (C) 2024 Nethesis S.r.l.
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
8+
import NeSkeleton from './NeSkeleton.vue'
9+
import NeInlineNotification from './NeInlineNotification.vue'
10+
import NeDropdown, { type NeDropdownItem } from './NeDropdown.vue'
11+
12+
const props = defineProps({
13+
title: {
14+
type: String,
15+
default: ''
16+
},
17+
description: {
18+
type: String,
19+
default: ''
20+
},
21+
icon: {
22+
type: Array<string>,
23+
default: () => []
24+
},
25+
loading: {
26+
type: Boolean
27+
},
28+
skeletonLines: {
29+
type: Number,
30+
default: 4
31+
},
32+
errorTitle: {
33+
type: String,
34+
default: ''
35+
},
36+
errorDescription: {
37+
type: String,
38+
default: ''
39+
},
40+
menuItems: {
41+
type: Array<NeDropdownItem>,
42+
default: () => []
43+
},
44+
alternateBackground: {
45+
type: Boolean
46+
}
47+
})
48+
49+
defineEmits(['titleClick'])
50+
</script>
51+
52+
<template>
53+
<div
54+
:class="[
55+
`overflow-hidden px-4 py-5 text-sm text-gray-700 dark:text-gray-200 sm:rounded-lg sm:px-6 sm:shadow`,
56+
props.alternateBackground ? 'bg-gray-50 dark:bg-gray-900' : 'bg-white dark:bg-gray-950'
57+
]"
58+
>
59+
<!-- header -->
60+
<div class="flex justify-between">
61+
<!-- title -->
62+
<h3
63+
v-if="title || $slots.title"
64+
class="mb-3 font-semibold leading-6 text-gray-900 dark:text-gray-50"
65+
>
66+
<span v-if="title">
67+
{{ title }}
68+
</span>
69+
<slot v-if="$slots.title" name="title"></slot>
70+
<span v-if="$slots.titleTooltip" class="ml-1">
71+
<slot name="titleTooltip"></slot>
72+
</span>
73+
</h3>
74+
<!-- top-right slot (e.g. for kebab menu) -->
75+
<div
76+
v-if="$slots.topRight || menuItems?.length"
77+
class="relative -right-1.5 -top-1.5 flex items-center"
78+
>
79+
<div v-if="$slots.topRight">
80+
<slot name="topRight"></slot>
81+
</div>
82+
<!-- top-right menu -->
83+
<div v-if="menuItems?.length">
84+
<NeDropdown :items="menuItems" :align-to-right="true" />
85+
</div>
86+
</div>
87+
</div>
88+
<!-- description and content -->
89+
<div class="flex flex-row items-center justify-between">
90+
<div class="grow">
91+
<NeSkeleton v-if="loading" :lines="skeletonLines"></NeSkeleton>
92+
<NeInlineNotification
93+
v-else-if="errorTitle"
94+
kind="error"
95+
:title="errorTitle"
96+
:description="errorDescription"
97+
/>
98+
<template v-else>
99+
<div v-if="description" class="mb-3 text-gray-500 dark:text-gray-400">
100+
{{ description }}
101+
</div>
102+
<slot></slot>
103+
</template>
104+
</div>
105+
<FontAwesomeIcon
106+
v-if="icon?.length"
107+
:icon="icon"
108+
class="ml-4 h-6 w-6 shrink-0 text-gray-400 dark:text-gray-600"
109+
/>
110+
</div>
111+
</div>
112+
</template>

src/components/NeDropdown.vue

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<!--
2+
Copyright (C) 2024 Nethesis S.r.l.
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
-->
5+
6+
<script lang="ts" setup>
7+
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
8+
import NeButton from './NeButton.vue'
9+
import { library } from '@fortawesome/fontawesome-svg-core'
10+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
11+
import { faEllipsisVertical as fasEllipsisVertical } from '@fortawesome/free-solid-svg-icons'
12+
import { ref, watch } from 'vue'
13+
14+
export interface Props {
15+
items: NeDropdownItem[]
16+
alignToRight: boolean
17+
openMenuAriaLabel?: string
18+
}
19+
20+
const props = withDefaults(defineProps<Props>(), {
21+
items: () => [],
22+
alignToRight: false,
23+
openMenuAriaLabel: 'Open menu'
24+
})
25+
26+
export interface NeDropdownItem {
27+
id: string
28+
label?: string
29+
icon?: string
30+
iconStyle?: string
31+
danger?: boolean
32+
action?: () => void
33+
disabled?: boolean
34+
}
35+
36+
library.add(fasEllipsisVertical)
37+
38+
function onItemClick(item: NeDropdownItem) {
39+
if (!item.disabled && item.action) {
40+
item.action()
41+
}
42+
}
43+
44+
function getMenuItemActiveClasses(item: NeDropdownItem) {
45+
if (item.danger) {
46+
return 'bg-rose-700 text-white dark:bg-rose-600 dark:text-white'
47+
} else {
48+
return 'bg-gray-100 dark:bg-gray-800'
49+
}
50+
}
51+
52+
const top = ref(0)
53+
const left = ref(0)
54+
const right = ref(0)
55+
const buttonRef = ref<InstanceType<typeof MenuButton> | null>(null)
56+
57+
function calculatePosition() {
58+
top.value = buttonRef.value?.$el.getBoundingClientRect().bottom + window.scrollY
59+
left.value = buttonRef.value?.$el.getBoundingClientRect().left - window.scrollX
60+
right.value =
61+
document.documentElement.clientWidth -
62+
buttonRef.value?.$el.getBoundingClientRect().right -
63+
window.scrollX
64+
}
65+
66+
watch(
67+
() => props.alignToRight,
68+
() => {
69+
calculatePosition()
70+
}
71+
)
72+
</script>
73+
74+
<template>
75+
<Menu as="div" class="relative inline-block text-left">
76+
<MenuButton
77+
ref="buttonRef"
78+
class="flex items-center text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-50"
79+
@click="calculatePosition()"
80+
>
81+
<span class="sr-only">{{ openMenuAriaLabel }}</span>
82+
<slot name="button">
83+
<!-- default kebab button -->
84+
<NeButton class="py-2" kind="tertiary">
85+
<font-awesome-icon
86+
:icon="['fas', 'ellipsis-vertical']"
87+
aria-hidden="true"
88+
class="h-5 w-5 shrink-0"
89+
/>
90+
</NeButton>
91+
</slot>
92+
</MenuButton>
93+
<Teleport to="body">
94+
<transition
95+
enter-active-class="transition ease-out duration-100"
96+
enter-from-class="transform opacity-0 scale-95"
97+
enter-to-class="transform opacity-100 scale-100"
98+
leave-active-class="transition ease-in duration-75"
99+
leave-from-class="transform opacity-100 scale-100"
100+
leave-to-class="transform opacity-0 scale-95"
101+
>
102+
<MenuItems
103+
:style="[
104+
{ top: top + 'px' },
105+
alignToRight ? { right: right + 'px' } : { left: left + 'px' }
106+
]"
107+
class="absolute z-50 mt-2.5 min-w-[10rem] rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none dark:bg-gray-950 dark:ring-gray-500/50"
108+
>
109+
<template v-for="item in items" :key="item.id">
110+
<!-- divider -->
111+
<hr
112+
v-if="item.id.includes('divider')"
113+
class="my-1 border-gray-200 dark:border-gray-700"
114+
/>
115+
<!-- item -->
116+
<MenuItem v-else v-slot="{ active }" :disabled="item.disabled">
117+
<a
118+
:class="[
119+
active ? getMenuItemActiveClasses(item) : '',
120+
'group flex items-center px-4 py-2 text-sm',
121+
item.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
122+
item.danger
123+
? 'text-rose-700 dark:text-rose-500'
124+
: 'text-gray-700 dark:text-gray-50'
125+
]"
126+
@click="onItemClick(item)"
127+
>
128+
<font-awesome-icon
129+
v-if="item.icon"
130+
:icon="[item.iconStyle || 'fas', item.icon]"
131+
aria-hidden="true"
132+
class="mr-2 h-5 w-5 shrink-0"
133+
/>
134+
{{ item.label }}
135+
</a>
136+
</MenuItem>
137+
</template>
138+
</MenuItems>
139+
</transition>
140+
</Teleport>
141+
</Menu>
142+
</template>

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export { default as NeTableBody } from '@/components/NeTableBody.vue'
2222
export { default as NeTableRow } from '@/components/NeTableRow.vue'
2323
export { default as NeTableCell } from '@/components/NeTableCell.vue'
2424
export { default as NeCombobox } from '@/components/NeCombobox.vue'
25+
export { default as NeDropdown } from '@/components/NeDropdown.vue'
26+
export { default as NeCard } from '@/components/NeCard.vue'
2527

2628
// types
2729
export type { NeComboboxOption } from '@/components/NeCombobox.vue'

0 commit comments

Comments
 (0)