Skip to content

Commit 8731287

Browse files
committed
feat: add NeCard component
1 parent b688eed commit 8731287

File tree

3 files changed

+335
-0
lines changed

3 files changed

+335
-0
lines changed

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/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ 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'
2525
export { default as NeDropdown } from '@/components/NeDropdown.vue'
26+
export { default as NeCard } from '@/components/NeCard.vue'
2627

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

stories/NeCard.stories.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Copyright (C) 2024 Nethesis S.r.l.
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
import type { Meta, StoryObj } from '@storybook/vue3'
5+
import { NeCard, NeTooltip, NeButton } from '../src/main'
6+
import { faHeart } from '@fortawesome/free-solid-svg-icons'
7+
import { library } from '@fortawesome/fontawesome-svg-core'
8+
9+
library.add(faHeart)
10+
11+
const meta = {
12+
title: 'Visual/NeCard',
13+
component: NeCard,
14+
tags: ['autodocs'],
15+
args: {
16+
title: 'Card title',
17+
description: '',
18+
icon: [],
19+
loading: false,
20+
skeletonLines: 4,
21+
errorTitle: '',
22+
errorDescription: '',
23+
menuItems: [],
24+
alternateBackground: false
25+
}
26+
} satisfies Meta<typeof NeCard>
27+
28+
export default meta
29+
type Story = StoryObj<typeof meta>
30+
31+
const defaultTemplate =
32+
'<NeCard v-bind="args">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</NeCard>'
33+
34+
export const Default: Story = {
35+
render: (args) => ({
36+
components: { NeCard },
37+
setup() {
38+
return { args }
39+
},
40+
template: defaultTemplate
41+
}),
42+
args: {}
43+
}
44+
45+
const titleSlotTemplate = `<NeCard v-bind="args">
46+
<template #title>
47+
<span>Card title</span>
48+
<span class='ml-1 text-gray-500 dark:text-gray-400'>
49+
(with slot)
50+
</span>
51+
</template>
52+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
53+
</NeCard>`
54+
55+
export const TitleSlot: Story = {
56+
render: (args) => ({
57+
components: { NeCard },
58+
setup() {
59+
return { args }
60+
},
61+
template: titleSlotTemplate
62+
}),
63+
args: { title: '' }
64+
}
65+
66+
export const WithDescription: Story = {
67+
render: (args) => ({
68+
components: { NeCard },
69+
setup() {
70+
return { args }
71+
},
72+
template: defaultTemplate
73+
}),
74+
args: { description: 'Card description' }
75+
}
76+
77+
export const WithIcon: Story = {
78+
render: (args) => ({
79+
components: { NeCard },
80+
setup() {
81+
return { args }
82+
},
83+
template: defaultTemplate
84+
}),
85+
args: { icon: ['fas', 'heart'] }
86+
}
87+
88+
export const Loading: Story = {
89+
render: (args) => ({
90+
components: { NeCard },
91+
setup() {
92+
return { args }
93+
},
94+
template: defaultTemplate
95+
}),
96+
args: { loading: true }
97+
}
98+
99+
export const Error: Story = {
100+
render: (args) => ({
101+
components: { NeCard },
102+
setup() {
103+
return { args }
104+
},
105+
template: defaultTemplate
106+
}),
107+
args: {
108+
errorTitle: 'Cannot retrieve data',
109+
errorDescription: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
110+
}
111+
}
112+
113+
const templateWithTooltip =
114+
'<NeCard v-bind="args">\
115+
<template #titleTooltip>\
116+
<NeTooltip>\
117+
<template #content>Tooltip</template>\
118+
</NeTooltip>\
119+
</template>\
120+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\
121+
</NeCard>'
122+
123+
export const WithTooltip: Story = {
124+
render: (args) => ({
125+
components: { NeCard, NeTooltip },
126+
setup() {
127+
return { args }
128+
},
129+
template: templateWithTooltip
130+
}),
131+
args: {}
132+
}
133+
134+
export const WithMenu: Story = {
135+
render: (args) => ({
136+
components: { NeCard },
137+
setup() {
138+
return { args }
139+
},
140+
template: defaultTemplate
141+
}),
142+
args: {
143+
menuItems: [
144+
{
145+
id: 'edit',
146+
label: 'Edit'
147+
},
148+
{
149+
id: 'delete',
150+
label: 'Delete',
151+
danger: true
152+
}
153+
]
154+
}
155+
}
156+
157+
const templateWithTopRightSlot = `<NeCard v-bind="args">
158+
<template #topRight>
159+
<NeButton kind='tertiary'>Button</NeButton>
160+
</template>
161+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
162+
</NeCard>`
163+
164+
export const WithTopRightSlot: Story = {
165+
render: (args) => ({
166+
components: { NeCard, NeButton },
167+
setup() {
168+
return { args }
169+
},
170+
template: templateWithTopRightSlot
171+
}),
172+
args: {}
173+
}
174+
175+
export const WithMenuAndTopRightSlot: Story = {
176+
render: (args) => ({
177+
components: { NeCard, NeButton },
178+
setup() {
179+
return { args }
180+
},
181+
template: templateWithTopRightSlot
182+
}),
183+
args: {
184+
menuItems: [
185+
{
186+
id: 'edit',
187+
label: 'Edit'
188+
},
189+
{
190+
id: 'delete',
191+
label: 'Delete',
192+
danger: true
193+
}
194+
]
195+
}
196+
}
197+
198+
const alternateBackgroundTemplate = `
199+
<div class="bg-white dark:bg-gray-950 p-12 flex flex-col text-sm gap-6">
200+
<div class="text-gray-700 dark:text-gray-200">
201+
Alternate background is useful to get contrast when the card is placed in a container with the same background as the default card background
202+
</div>
203+
<NeCard v-bind="args" :alternateBackground="false">
204+
Card with default background
205+
</NeCard>
206+
<NeCard v-bind="args">
207+
Card with alternate background
208+
</NeCard>
209+
</div>`
210+
211+
export const AlternateBackground: Story = {
212+
render: (args) => ({
213+
components: { NeCard, NeButton },
214+
setup() {
215+
return { args }
216+
},
217+
template: alternateBackgroundTemplate
218+
}),
219+
args: {
220+
alternateBackground: true
221+
}
222+
}

0 commit comments

Comments
 (0)