Skip to content

Commit

Permalink
feat: add NeModal component (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
andre8244 authored Apr 22, 2024
1 parent 8b7587e commit 600c2dc
Show file tree
Hide file tree
Showing 3 changed files with 389 additions and 0 deletions.
209 changes: 209 additions & 0 deletions src/components/NeModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script lang="ts" setup>
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { faXmark as fasXmark } from '@fortawesome/free-solid-svg-icons'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import NeButton, { type ButtonKind } from './NeButton.vue'
import NeRoundedIcon from './NeRoundedIcon.vue'
import type { PropType } from 'vue'

type ModalKind = 'neutral' | 'info' | 'warning' | 'error' | 'success'
type PrimaryButtonKind = 'primary' | 'danger'
type ModalSize = 'md' | 'lg' | 'xl' | 'xxl'

defineProps({
visible: {
type: Boolean,
required: true
},
title: {
type: String,
default: ''
},
kind: {
type: String as PropType<ModalKind>,
default: 'neutral'
},
size: {
type: String as PropType<ModalSize>,
default: 'md'
},
primaryLabel: {
type: String,
default: ''
},
secondaryLabel: {
type: String,
default: ''
},
cancelLabel: {
type: String,
default: ''
},
primaryButtonKind: {
type: String as PropType<PrimaryButtonKind>,
default: 'primary'
},
primaryButtonDisabled: {
type: Boolean,
default: false
},
primaryButtonLoading: {
type: Boolean,
default: false
},
secondaryButtonKind: {
type: String as PropType<ButtonKind>,
default: 'secondary'
},
secondaryButtonDisabled: {
type: Boolean,
default: false
},
secondaryButtonLoading: {
type: Boolean,
default: false
},
closeAriaLabel: {
type: String,
required: true
}
})

const emit = defineEmits(['close', 'primaryClick', 'secondaryClick'])

// add fontawesome icons
library.add(fasXmark)

const sizeStyle: Record<ModalSize, string> = {
md: 'sm:max-w-lg',
lg: 'sm:max-w-2xl',
xl: 'sm:max-w-4xl',
xxl: 'sm:max-w-6xl'
}

function onClose() {
emit('close')
}

function onPrimaryClick() {
emit('primaryClick')
}

function onSecondaryClick() {
emit('secondaryClick')
}
</script>

<template>
<TransitionRoot as="template" :show="visible">
<Dialog as="div" class="relative z-[100]" @close="onClose">
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in duration-200"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-700 dark:bg-opacity-75"
/>
</TransitionChild>

<div class="fixed inset-0 z-[100] overflow-y-auto">
<div
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
>
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<DialogPanel
:class="`relative transform overflow-hidden rounded-lg px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full ${sizeStyle[size]} bg-gray-50 dark:bg-gray-900 sm:p-6`"
>
<form @submit.prevent>
<div class="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
<button
type="button"
class="rounded-md p-1 leading-none text-gray-600 transition-colors hover:bg-gray-200 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-white dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus:ring-primary-300 dark:focus:ring-offset-gray-900"
@click="onClose"
>
<span class="sr-only">{{ closeAriaLabel }}</span>
<font-awesome-icon
:icon="['fas', 'xmark']"
class="h-5 w-5"
aria-hidden="true"
/>
</button>
</div>
<div class="sm:flex sm:items-start">
<template v-if="kind !== 'neutral'">
<NeRoundedIcon
:kind="kind"
class="mx-auto mb-3 flex-shrink-0 sm:mx-0 sm:mb-0 sm:mr-4"
/>
</template>
<div class="grow text-center sm:text-left">
<DialogTitle
v-if="title"
as="h3"
class="mb-4 text-base font-semibold leading-6 text-gray-900 dark:text-gray-50"
>{{ title }}</DialogTitle
>
<div>
<p class="text-sm text-gray-700 dark:text-gray-200">
<slot />
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<NeButton
:kind="primaryButtonKind"
size="lg"
:disabled="primaryButtonDisabled"
:loading="primaryButtonLoading"
type="submit"
class="w-full sm:ml-3 sm:w-auto"
@click.prevent="onPrimaryClick"
>{{ primaryLabel }}</NeButton
>
<NeButton
v-if="secondaryLabel"
:kind="secondaryButtonKind"
size="lg"
:disabled="secondaryButtonDisabled"
:loading="secondaryButtonLoading"
class="mt-3 w-full sm:ml-3 sm:mt-0 sm:w-auto"
@click="onSecondaryClick"
>{{ secondaryLabel }}</NeButton
>
<NeButton
v-if="cancelLabel"
kind="tertiary"
size="lg"
class="mt-3 w-full sm:mt-0 sm:w-auto"
@click="onClose"
>{{ cancelLabel }}</NeButton
>
</div>
</form>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export { default as NeTextArea } from '@/components/NeTextArea.vue'
export { default as NeTextInput } from '@/components/NeTextInput.vue'
export { default as NeToggle } from '@/components/NeToggle.vue'
export { default as NeToastNotification } from '@/components/NeToastNotification.vue'
export { default as NeModal } from '@/components/NeModal.vue'

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

import type { Meta, StoryObj } from '@storybook/vue3'

import { NeModal } from '../src/main'

const meta = {
title: 'Visual/NeModal',
component: NeModal,
argTypes: {
kind: { control: 'inline-radio', options: ['neutral', 'info', 'warning', 'error', 'success'] },
size: { control: 'inline-radio', options: ['md', 'lg', 'xl', 'xxl'] },
primaryButtonKind: { control: 'inline-radio', options: ['primary', 'danger'] },
secondaryButtonKind: {
control: 'inline-radio',
options: ['primary', 'secondary', 'tertiary', 'danger']
}
},
args: {
visible: true,
title: 'Title',
kind: 'neutral',
size: 'md',
primaryLabel: 'Primary',
secondaryLabel: '',
cancelLabel: 'Cancel',
primaryButtonKind: 'primary',
primaryButtonDisabled: false,
primaryButtonLoading: false,
secondaryButtonKind: 'secondary',
secondaryButtonDisabled: false,
secondaryButtonLoading: false,
closeAriaLabel: 'Close'
} // default values
} satisfies Meta<typeof NeModal>

export default meta
type Story = StoryObj<typeof meta>

const template = `<NeModal v-bind="args">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua
</p>
</NeModal>`

export const Neutral: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: template
}),
args: {}
}

export const Info: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: template
}),
args: {
kind: 'info'
}
}

const warningTemplate = `<NeModal v-bind="args">
<p>
Delete user <strong>John Doe?</strong> This action cannot be undone
</p>
</NeModal>`

export const Warning: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: warningTemplate
}),
args: {
kind: 'warning',
primaryLabel: 'Delete',
primaryButtonKind: 'danger'
}
}

export const Error: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: template
}),
args: {
kind: 'error'
}
}

export const Success: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: template
}),
args: {
kind: 'success'
}
}

const singleButtonTemplate = `<NeModal v-bind="args">
<p>
Your system is up to date
</p>
</NeModal>`

export const SingleButton: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: singleButtonTemplate
}),
args: {
title: 'System updates',
primaryLabel: 'Got it',
cancelLabel: ''
}
}

export const ThreeButtons: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: template
}),
args: {
primaryLabel: 'Primary',
secondaryLabel: 'Secondary'
}
}

export const PrimaryLoading: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: template
}),
args: {
primaryLabel: 'Save',
primaryButtonLoading: true,
primaryButtonDisabled: true
}
}

export const Large: Story = {
render: (args) => ({
components: { NeModal },
setup() {
return { args }
},
template: template
}),
args: {
size: 'lg'
}
}

0 comments on commit 600c2dc

Please sign in to comment.