fix: abstracted modal's common code to BaseModal

This commit is contained in:
2026-08-31 18:09:02 +02:00
parent ca0317a01a
commit 2cc38bbc17
4 changed files with 219 additions and 384 deletions
+122
View File
@@ -0,0 +1,122 @@
<script setup lang="ts">
import { useEscapeKey } from '@/composables/useEscapeKey'
defineProps<{
title: string
titleId: string
}>()
const emit = defineEmits<{
close: []
}>()
useEscapeKey(handleClose)
function handleClose() {
emit('close')
}
</script>
<template>
<div class="modal-overlay" @click.self="handleClose">
<div class="modal-card card" role="dialog" aria-modal="true" :aria-labelledby="titleId">
<div class="modal-header">
<h3 :id="titleId">{{ title }}</h3>
<button type="button" class="close-btn" aria-label="Close modal" @click="handleClose">
</button>
</div>
<slot />
<div class="modal-footer">
<slot name="footer" />
</div>
</div>
</div>
</template>
<style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 100;
animation: fadeIn 0.15s ease-out;
}
.modal-card {
width: 100%;
max-width: 440px;
background-color: var(--c-bg-soft);
border: 1px solid var(--c-border-hover);
border-radius: var(--radius-lg);
padding: 1.25rem 1.4rem;
box-shadow: var(--shadow-md);
animation: slideUp 0.15s ease-out;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.modal-header h3 {
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: var(--c-text-soft);
font-size: 1.1rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
border-radius: var(--radius-sm);
line-height: 1;
}
.close-btn:hover {
color: var(--c-heading);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
margin-top: 1rem;
}
.modal-footer .btn {
width: auto;
padding: 0.5rem 1.2rem;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
</style>
+5 -103
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import { changePasswordApi } from '@/api/users' import { changePasswordApi } from '@/api/users'
import { useEscapeKey } from '@/composables/useEscapeKey' import BaseModal from '@/components/BaseModal.vue'
const emit = defineEmits<{ const emit = defineEmits<{
close: [] close: []
@@ -15,8 +15,6 @@ const isSubmitting = ref(false)
const error = ref('') const error = ref('')
const successMessage = ref('') const successMessage = ref('')
useEscapeKey(handleClose)
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
passwordInput.value?.focus() passwordInput.value?.focus()
@@ -59,20 +57,7 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<div class="modal-overlay" @click.self="handleClose"> <BaseModal title="Change password" title-id="change-password-modal-title" @close="handleClose">
<div
class="modal-card card"
role="dialog"
aria-modal="true"
aria-labelledby="change-password-modal-title"
>
<div class="modal-header">
<h3 id="change-password-modal-title">Change password</h3>
<button type="button" class="close-btn" aria-label="Close modal" @click="handleClose">
</button>
</div>
<p class="modal-description">Enter your current password and choose a new one.</p> <p class="modal-description">Enter your current password and choose a new one.</p>
<form class="password-form" @submit.prevent="handleSubmit"> <form class="password-form" @submit.prevent="handleSubmit">
@@ -116,65 +101,13 @@ async function handleSubmit() {
<p v-if="error" class="banner banner-error">{{ error }}</p> <p v-if="error" class="banner banner-error">{{ error }}</p>
<p v-if="successMessage" class="banner banner-success">{{ successMessage }}</p> <p v-if="successMessage" class="banner banner-success">{{ successMessage }}</p>
<div class="modal-footer"> <template #footer>
<button type="button" class="btn btn-secondary" @click="handleClose">Done</button> <button type="button" class="btn btn-secondary" @click="handleClose">Done</button>
</div> </template>
</div> </BaseModal>
</div>
</template> </template>
<style scoped> <style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 100;
animation: fadeIn 0.15s ease-out;
}
.modal-card {
width: 100%;
max-width: 440px;
background-color: var(--c-bg-soft);
border: 1px solid var(--c-border-hover);
border-radius: var(--radius-lg);
padding: 1.25rem 1.4rem;
box-shadow: var(--shadow-md);
animation: slideUp 0.15s ease-out;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.modal-header h3 {
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: var(--c-text-soft);
font-size: 1.1rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
border-radius: var(--radius-sm);
line-height: 1;
}
.close-btn:hover {
color: var(--c-heading);
}
.modal-description { .modal-description {
font-size: 0.85rem; font-size: 0.85rem;
color: var(--c-text-soft); color: var(--c-text-soft);
@@ -192,17 +125,6 @@ async function handleSubmit() {
padding: 0.65rem 1.2rem; padding: 0.65rem 1.2rem;
} }
.modal-footer {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
}
.modal-footer .btn {
width: auto;
padding: 0.5rem 1.2rem;
}
.banner-success { .banner-success {
background-color: rgba(52, 211, 153, 0.12); background-color: rgba(52, 211, 153, 0.12);
border: 1px solid rgba(52, 211, 153, 0.4); border: 1px solid rgba(52, 211, 153, 0.4);
@@ -213,24 +135,4 @@ async function handleSubmit() {
.banner-error { .banner-error {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
</style> </style>
+10 -105
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LocalList } from '@/database/db' import type { LocalList } from '@/database/db'
import { useEscapeKey } from '@/composables/useEscapeKey' import BaseModal from '@/components/BaseModal.vue'
defineProps<{ defineProps<{
list: LocalList list: LocalList
@@ -11,8 +11,6 @@ const emit = defineEmits<{
confirm: [] confirm: []
}>() }>()
useEscapeKey(handleClose)
function handleClose() { function handleClose() {
emit('close') emit('close')
} }
@@ -23,125 +21,32 @@ function handleConfirm() {
</script> </script>
<template> <template>
<div class="modal-overlay" @click.self="handleClose"> <BaseModal
<div :title="`Delete &quot;${list.name}&quot;?`"
class="modal-card card" title-id="delete-modal-title"
role="dialog" @close="handleClose"
aria-modal="true"
aria-labelledby="delete-modal-title"
> >
<div class="modal-header">
<h3 id="delete-modal-title">Delete "{{ list.name }}"?</h3>
<button type="button" class="close-btn" aria-label="Close modal" @click="handleClose">
</button>
</div>
<p class="modal-description"> <p class="modal-description">
Are you sure you want to delete this list? This action cannot be undone and all items in Are you sure you want to delete this list? This action cannot be undone and all items in this
this list will be deleted. list will be deleted.
</p> </p>
<div class="modal-footer"> <template #footer>
<button type="button" class="btn btn-secondary cancel-btn" @click="handleClose"> <button type="button" class="btn btn-secondary cancel-btn" @click="handleClose">
Cancel Cancel
</button> </button>
<button type="button" class="btn btn-danger confirm-delete-btn" @click="handleConfirm"> <button type="button" class="btn btn-danger confirm-delete-btn" @click="handleConfirm">
Delete list Delete list
</button> </button>
</div> </template>
</div> </BaseModal>
</div>
</template> </template>
<style scoped> <style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 100;
animation: fadeIn 0.15s ease-out;
}
.modal-card {
width: 100%;
max-width: 440px;
background-color: var(--c-bg-soft);
border: 1px solid var(--c-border-hover);
border-radius: var(--radius-lg);
padding: 1.25rem 1.4rem;
box-shadow: var(--shadow-md);
animation: slideUp 0.15s ease-out;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.modal-header h3 {
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: var(--c-text-soft);
font-size: 1.1rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
border-radius: var(--radius-sm);
line-height: 1;
}
.close-btn:hover {
color: var(--c-heading);
}
.modal-description { .modal-description {
font-size: 0.85rem; font-size: 0.85rem;
color: var(--c-text-soft); color: var(--c-text-soft);
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
line-height: 1.4; line-height: 1.4;
} }
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
margin-top: 1rem;
}
.modal-footer .btn {
width: auto;
padding: 0.5rem 1.2rem;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
</style> </style>
+8 -102
View File
@@ -2,7 +2,7 @@
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import type { LocalList } from '@/database/db' import type { LocalList } from '@/database/db'
import { useListsStore } from '@/stores/lists' import { useListsStore } from '@/stores/lists'
import { useEscapeKey } from '@/composables/useEscapeKey' import BaseModal from '@/components/BaseModal.vue'
const props = defineProps<{ const props = defineProps<{
list: LocalList list: LocalList
@@ -20,8 +20,6 @@ const isSubmitting = ref(false)
const error = ref('') const error = ref('')
const successMessage = ref('') const successMessage = ref('')
useEscapeKey(handleClose)
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
emailInput.value?.focus() emailInput.value?.focus()
@@ -53,20 +51,11 @@ async function handleAddUser() {
</script> </script>
<template> <template>
<div class="modal-overlay" @click.self="handleClose"> <BaseModal
<div :title="`Share &quot;${list.name}&quot;`"
class="modal-card card" title-id="share-modal-title"
role="dialog" @close="handleClose"
aria-modal="true"
aria-labelledby="share-modal-title"
> >
<div class="modal-header">
<h3 id="share-modal-title">Share "{{ list.name }}"</h3>
<button type="button" class="close-btn" aria-label="Close modal" @click="handleClose">
</button>
</div>
<p class="modal-description">Enter an email to invite them to collaborate on this list.</p> <p class="modal-description">Enter an email to invite them to collaborate on this list.</p>
<form class="share-form" @submit.prevent="handleAddUser"> <form class="share-form" @submit.prevent="handleAddUser">
@@ -91,65 +80,13 @@ async function handleAddUser() {
<p v-if="error" class="banner banner-error">{{ error }}</p> <p v-if="error" class="banner banner-error">{{ error }}</p>
<p v-if="successMessage" class="banner banner-success">{{ successMessage }}</p> <p v-if="successMessage" class="banner banner-success">{{ successMessage }}</p>
<div class="modal-footer"> <template #footer>
<button type="button" class="btn btn-secondary" @click="handleClose">Done</button> <button type="button" class="btn btn-secondary" @click="handleClose">Done</button>
</div> </template>
</div> </BaseModal>
</div>
</template> </template>
<style scoped> <style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 100;
animation: fadeIn 0.15s ease-out;
}
.modal-card {
width: 100%;
max-width: 440px;
background-color: var(--c-bg-soft);
border: 1px solid var(--c-border-hover);
border-radius: var(--radius-lg);
padding: 1.25rem 1.4rem;
box-shadow: var(--shadow-md);
animation: slideUp 0.15s ease-out;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.modal-header h3 {
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: var(--c-text-soft);
font-size: 1.1rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
border-radius: var(--radius-sm);
line-height: 1;
}
.close-btn:hover {
color: var(--c-heading);
}
.modal-description { .modal-description {
font-size: 0.85rem; font-size: 0.85rem;
color: var(--c-text-soft); color: var(--c-text-soft);
@@ -172,17 +109,6 @@ async function handleAddUser() {
padding: 0.65rem 1.2rem; padding: 0.65rem 1.2rem;
} }
.modal-footer {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
}
.modal-footer .btn {
width: auto;
padding: 0.5rem 1.2rem;
}
.banner-success { .banner-success {
background-color: rgba(52, 211, 153, 0.12); background-color: rgba(52, 211, 153, 0.12);
border: 1px solid rgba(52, 211, 153, 0.4); border: 1px solid rgba(52, 211, 153, 0.4);
@@ -193,24 +119,4 @@ async function handleAddUser() {
.banner-error { .banner-error {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
</style> </style>