Skip to content

Commit

Permalink
feat: add NeCard and NeDropdown #12
Browse files Browse the repository at this point in the history
Add NeDropdown and NeCard components
  • Loading branch information
andre8244 authored Jan 22, 2024
2 parents b3f294f + 8731287 commit 661c682
Show file tree
Hide file tree
Showing 7 changed files with 604 additions and 0 deletions.
1 change: 1 addition & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Preview, VueRenderer } from '@storybook/vue3'
import { withThemeByClassName } from '@storybook/addon-themes'

import '../src/main.css'
import './storybook.css'

const preview: Preview = {
parameters: {
Expand Down
9 changes: 9 additions & 0 deletions .storybook/storybook.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.sb-show-main.sb-main-padded {
/* remove default padding from component area */
padding: 0;
}

#storybook-root {
/* set background color according to theme */
@apply h-screen bg-gray-50 dark:bg-gray-900;
}
112 changes: 112 additions & 0 deletions src/components/NeCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script setup lang="ts">
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import NeSkeleton from './NeSkeleton.vue'
import NeInlineNotification from './NeInlineNotification.vue'
import NeDropdown, { type NeDropdownItem } from './NeDropdown.vue'

const props = defineProps({
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
icon: {
type: Array<string>,
default: () => []
},
loading: {
type: Boolean
},
skeletonLines: {
type: Number,
default: 4
},
errorTitle: {
type: String,
default: ''
},
errorDescription: {
type: String,
default: ''
},
menuItems: {
type: Array<NeDropdownItem>,
default: () => []
},
alternateBackground: {
type: Boolean
}
})

defineEmits(['titleClick'])
</script>

<template>
<div
:class="[
`overflow-hidden px-4 py-5 text-sm text-gray-700 dark:text-gray-200 sm:rounded-lg sm:px-6 sm:shadow`,
props.alternateBackground ? 'bg-gray-50 dark:bg-gray-900' : 'bg-white dark:bg-gray-950'
]"
>
<!-- header -->
<div class="flex justify-between">
<!-- title -->
<h3
v-if="title || $slots.title"
class="mb-3 font-semibold leading-6 text-gray-900 dark:text-gray-50"
>
<span v-if="title">
{{ title }}
</span>
<slot v-if="$slots.title" name="title"></slot>
<span v-if="$slots.titleTooltip" class="ml-1">
<slot name="titleTooltip"></slot>
</span>
</h3>
<!-- top-right slot (e.g. for kebab menu) -->
<div
v-if="$slots.topRight || menuItems?.length"
class="relative -right-1.5 -top-1.5 flex items-center"
>
<div v-if="$slots.topRight">
<slot name="topRight"></slot>
</div>
<!-- top-right menu -->
<div v-if="menuItems?.length">
<NeDropdown :items="menuItems" :align-to-right="true" />
</div>
</div>
</div>
<!-- description and content -->
<div class="flex flex-row items-center justify-between">
<div class="grow">
<NeSkeleton v-if="loading" :lines="skeletonLines"></NeSkeleton>
<NeInlineNotification
v-else-if="errorTitle"
kind="error"
:title="errorTitle"
:description="errorDescription"
/>
<template v-else>
<div v-if="description" class="mb-3 text-gray-500 dark:text-gray-400">
{{ description }}
</div>
<slot></slot>
</template>
</div>
<FontAwesomeIcon
v-if="icon?.length"
:icon="icon"
class="ml-4 h-6 w-6 shrink-0 text-gray-400 dark:text-gray-600"
/>
</div>
</div>
</template>
142 changes: 142 additions & 0 deletions src/components/NeDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script lang="ts" setup>
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
import NeButton from './NeButton.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faEllipsisVertical as fasEllipsisVertical } from '@fortawesome/free-solid-svg-icons'
import { ref, watch } from 'vue'

export interface Props {
items: NeDropdownItem[]
alignToRight: boolean
openMenuAriaLabel?: string
}

const props = withDefaults(defineProps<Props>(), {
items: () => [],
alignToRight: false,
openMenuAriaLabel: 'Open menu'
})

export interface NeDropdownItem {
id: string
label?: string
icon?: string
iconStyle?: string
danger?: boolean
action?: () => void
disabled?: boolean
}

library.add(fasEllipsisVertical)

function onItemClick(item: NeDropdownItem) {
if (!item.disabled && item.action) {
item.action()
}
}

function getMenuItemActiveClasses(item: NeDropdownItem) {
if (item.danger) {
return 'bg-rose-700 text-white dark:bg-rose-600 dark:text-white'
} else {
return 'bg-gray-100 dark:bg-gray-800'
}
}

const top = ref(0)
const left = ref(0)
const right = ref(0)
const buttonRef = ref<InstanceType<typeof MenuButton> | null>(null)

function calculatePosition() {
top.value = buttonRef.value?.$el.getBoundingClientRect().bottom + window.scrollY
left.value = buttonRef.value?.$el.getBoundingClientRect().left - window.scrollX
right.value =
document.documentElement.clientWidth -
buttonRef.value?.$el.getBoundingClientRect().right -
window.scrollX
}

watch(
() => props.alignToRight,
() => {
calculatePosition()
}
)
</script>

<template>
<Menu as="div" class="relative inline-block text-left">
<MenuButton
ref="buttonRef"
class="flex items-center text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-50"
@click="calculatePosition()"
>
<span class="sr-only">{{ openMenuAriaLabel }}</span>
<slot name="button">
<!-- default kebab button -->
<NeButton class="py-2" kind="tertiary">
<font-awesome-icon
:icon="['fas', 'ellipsis-vertical']"
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
</NeButton>
</slot>
</MenuButton>
<Teleport to="body">
<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
:style="[
{ top: top + 'px' },
alignToRight ? { right: right + 'px' } : { left: left + 'px' }
]"
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"
>
<template v-for="item in items" :key="item.id">
<!-- divider -->
<hr
v-if="item.id.includes('divider')"
class="my-1 border-gray-200 dark:border-gray-700"
/>
<!-- item -->
<MenuItem v-else v-slot="{ active }" :disabled="item.disabled">
<a
:class="[
active ? getMenuItemActiveClasses(item) : '',
'group flex items-center px-4 py-2 text-sm',
item.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
item.danger
? 'text-rose-700 dark:text-rose-500'
: 'text-gray-700 dark:text-gray-50'
]"
@click="onItemClick(item)"
>
<font-awesome-icon
v-if="item.icon"
:icon="[item.iconStyle || 'fas', item.icon]"
aria-hidden="true"
class="mr-2 h-5 w-5 shrink-0"
/>
{{ item.label }}
</a>
</MenuItem>
</template>
</MenuItems>
</transition>
</Teleport>
</Menu>
</template>
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export { default as NeTableBody } from '@/components/NeTableBody.vue'
export { default as NeTableRow } from '@/components/NeTableRow.vue'
export { default as NeTableCell } from '@/components/NeTableCell.vue'
export { default as NeCombobox } from '@/components/NeCombobox.vue'
export { default as NeDropdown } from '@/components/NeDropdown.vue'
export { default as NeCard } from '@/components/NeCard.vue'

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

0 comments on commit 661c682

Please sign in to comment.