Abstracted modal's common code to BaseModal #11
@@ -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>
|
||||||
@@ -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,122 +57,57 @@ 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
|
<p class="modal-description">Enter your current password and choose a new one.</p>
|
||||||
class="modal-card card"
|
|
||||||
role="dialog"
|
<form class="password-form" @submit.prevent="handleSubmit">
|
||||||
aria-modal="true"
|
<div class="field">
|
||||||
aria-labelledby="change-password-modal-title"
|
<input
|
||||||
>
|
ref="passwordInput"
|
||||||
<div class="modal-header">
|
v-model="currentPassword"
|
||||||
<h3 id="change-password-modal-title">Change password</h3>
|
type="password"
|
||||||
<button type="button" class="close-btn" aria-label="Close modal" @click="handleClose">
|
placeholder="Current password"
|
||||||
✕
|
autocomplete="current-password"
|
||||||
</button>
|
:disabled="isSubmitting"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
<p class="modal-description">Enter your current password and choose a new one.</p>
|
<input
|
||||||
|
v-model="newPassword"
|
||||||
<form class="password-form" @submit.prevent="handleSubmit">
|
type="password"
|
||||||
<div class="field">
|
placeholder="New password"
|
||||||
<input
|
autocomplete="new-password"
|
||||||
ref="passwordInput"
|
:disabled="isSubmitting"
|
||||||
v-model="currentPassword"
|
/>
|
||||||
type="password"
|
|
||||||
placeholder="Current password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<input
|
|
||||||
v-model="newPassword"
|
|
||||||
type="password"
|
|
||||||
placeholder="New password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<input
|
|
||||||
v-model="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
placeholder="Confirm new password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary"
|
|
||||||
:disabled="isSubmitting || !currentPassword || !newPassword || !confirmPassword"
|
|
||||||
>
|
|
||||||
Change password
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p v-if="error" class="banner banner-error">{{ error }}</p>
|
|
||||||
<p v-if="successMessage" class="banner banner-success">{{ successMessage }}</p>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" @click="handleClose">Done</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="field">
|
||||||
</div>
|
<input
|
||||||
|
v-model="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="isSubmitting || !currentPassword || !newPassword || !confirmPassword"
|
||||||
|
>
|
||||||
|
Change password
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p v-if="error" class="banner banner-error">{{ error }}</p>
|
||||||
|
<p v-if="successMessage" class="banner banner-success">{{ successMessage }}</p>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<button type="button" class="btn btn-secondary" @click="handleClose">Done</button>
|
||||||
|
</template>
|
||||||
|
</BaseModal>
|
||||||
</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>
|
||||||
|
|||||||
@@ -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 "${list.name}"?`"
|
||||||
class="modal-card card"
|
title-id="delete-modal-title"
|
||||||
role="dialog"
|
@close="handleClose"
|
||||||
aria-modal="true"
|
>
|
||||||
aria-labelledby="delete-modal-title"
|
<p class="modal-description">
|
||||||
>
|
Are you sure you want to delete this list? This action cannot be undone and all items in this
|
||||||
<div class="modal-header">
|
list will be deleted.
|
||||||
<h3 id="delete-modal-title">Delete "{{ list.name }}"?</h3>
|
</p>
|
||||||
<button type="button" class="close-btn" aria-label="Close modal" @click="handleClose">
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="modal-description">
|
<template #footer>
|
||||||
Are you sure you want to delete this list? This action cannot be undone and all items in
|
<button type="button" class="btn btn-secondary cancel-btn" @click="handleClose">
|
||||||
this list will be deleted.
|
Cancel
|
||||||
</p>
|
</button>
|
||||||
|
<button type="button" class="btn btn-danger confirm-delete-btn" @click="handleConfirm">
|
||||||
<div class="modal-footer">
|
Delete list
|
||||||
<button type="button" class="btn btn-secondary cancel-btn" @click="handleClose">
|
</button>
|
||||||
Cancel
|
</template>
|
||||||
</button>
|
</BaseModal>
|
||||||
<button type="button" class="btn btn-danger confirm-delete-btn" @click="handleConfirm">
|
|
||||||
Delete list
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -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,103 +51,42 @@ async function handleAddUser() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="modal-overlay" @click.self="handleClose">
|
<BaseModal
|
||||||
<div
|
:title="`Share "${list.name}"`"
|
||||||
class="modal-card card"
|
title-id="share-modal-title"
|
||||||
role="dialog"
|
@close="handleClose"
|
||||||
aria-modal="true"
|
>
|
||||||
aria-labelledby="share-modal-title"
|
<p class="modal-description">Enter an email to invite them to collaborate on this list.</p>
|
||||||
>
|
|
||||||
<div class="modal-header">
|
<form class="share-form" @submit.prevent="handleAddUser">
|
||||||
<h3 id="share-modal-title">Share "{{ list.name }}"</h3>
|
<div class="field">
|
||||||
<button type="button" class="close-btn" aria-label="Close modal" @click="handleClose">
|
<input
|
||||||
✕
|
ref="emailInput"
|
||||||
</button>
|
v-model="newEmail"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary share-btn"
|
||||||
|
:disabled="isSubmitting || !newEmail.trim()"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<p class="modal-description">Enter an email to invite them to collaborate on this list.</p>
|
<p v-if="error" class="banner banner-error">{{ error }}</p>
|
||||||
|
<p v-if="successMessage" class="banner banner-success">{{ successMessage }}</p>
|
||||||
|
|
||||||
<form class="share-form" @submit.prevent="handleAddUser">
|
<template #footer>
|
||||||
<div class="field">
|
<button type="button" class="btn btn-secondary" @click="handleClose">Done</button>
|
||||||
<input
|
</template>
|
||||||
ref="emailInput"
|
</BaseModal>
|
||||||
v-model="newEmail"
|
|
||||||
type="email"
|
|
||||||
placeholder="Email"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary share-btn"
|
|
||||||
:disabled="isSubmitting || !newEmail.trim()"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p v-if="error" class="banner banner-error">{{ error }}</p>
|
|
||||||
<p v-if="successMessage" class="banner banner-success">{{ successMessage }}</p>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" @click="handleClose">Done</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user