Update modals to use shared wrapper (#1404)

This commit is contained in:
Leendert de Borst
2025-12-14 08:22:41 +01:00
parent fd4656afe5
commit 3eb39c2968
17 changed files with 718 additions and 723 deletions

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/Forms/FormInputCopyToClipboard';
import { useDb } from '@/entrypoints/popup/context/DbContext';
@@ -56,10 +57,6 @@ const FieldHistoryModal: React.FC<FieldHistoryModalProps> = ({
}
}, [isOpen, dbContext?.sqliteClient, itemId, fieldKey]);
if (!isOpen) {
return null;
}
/**
* Format a date string to a human readable format.
*/
@@ -89,87 +86,52 @@ const FieldHistoryModal: React.FC<FieldHistoryModalProps> = ({
}
};
/**
* Handle click on backdrop to close modal.
*/
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>): void => {
// Only close if clicking directly on the backdrop/container, not the modal content
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div className="fixed inset-0 bg-black bg-opacity-80 transition-opacity" onClick={onClose} />
{/* Modal container - clicking here (outside modal content) closes */}
<div
className="fixed inset-0 flex items-center justify-center p-4"
onClick={handleBackdropClick}
>
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all w-full max-w-2xl max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
{fieldLabel} {t('credentials.history')}
</h3>
<button
type="button"
className="text-gray-400 hover:text-gray-500 focus:outline-none"
onClick={onClose}
>
<span className="sr-only">{t('common.close')}</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="overflow-y-auto flex-1">
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
</div>
) : history.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
{t('credentials.noHistoryAvailable')}
</div>
) : (
<div className="space-y-3">
{history.map((record) => {
const values = parseValueSnapshot(record.ValueSnapshot);
return (
<div
key={record.Id}
className="rounded-lg p-4 border border-gray-200 dark:border-gray-700"
>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{formatDate(record.ChangedAt)}
</div>
{values.map((value, idx) => (
<div key={idx} className="mt-2">
<FormInputCopyToClipboard
id={`history-${record.Id}-${idx}`}
label=""
value={value}
type={shouldMaskByDefault ? 'password' : 'text'}
/>
</div>
))}
</div>
);
})}
</div>
)}
</div>
<ModalWrapper
isOpen={isOpen}
onClose={onClose}
title={`${fieldLabel} ${t('credentials.history')}`}
maxWidth="max-w-2xl"
bodyClassName="px-4 pb-4 overflow-y-auto max-h-[60vh]"
>
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
</div>
</div>
</div>
) : history.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
{t('credentials.noHistoryAvailable')}
</div>
) : (
<div className="space-y-3">
{history.map((record) => {
const values = parseValueSnapshot(record.ValueSnapshot);
return (
<div
key={record.Id}
className="rounded-lg p-4 border border-gray-200 dark:border-gray-700"
>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{formatDate(record.ChangedAt)}
</div>
{values.map((value, idx) => (
<div key={idx} className="mt-2">
<FormInputCopyToClipboard
id={`history-${record.Id}-${idx}`}
label=""
value={value}
type={shouldMaskByDefault ? 'password' : 'text'}
/>
</div>
))}
</div>
);
})}
</div>
)}
</ModalWrapper>
);
};

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
type ConfirmDeleteModalProps = {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText: string;
}
/**
* A reusable confirmation modal for delete actions.
* Uses the danger styling to indicate a destructive action.
*/
const ConfirmDeleteModal: React.FC<ConfirmDeleteModalProps> = ({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText
}) => {
const { t } = useTranslation();
return (
<ModalWrapper
isOpen={isOpen}
onClose={onClose}
title={title}
maxWidth="max-w-sm"
footer={
<div className="flex justify-end gap-2">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
{t('common.cancel')}
</button>
<button
onClick={onConfirm}
className="px-4 py-2 text-sm bg-red-600 text-white rounded hover:bg-red-700"
>
{confirmText}
</button>
</div>
}
>
<p className="text-gray-500 dark:text-gray-400">
{message}
</p>
</ModalWrapper>
);
};
export default ConfirmDeleteModal;

View File

@@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
type HelpModalProps = {
title: string;
content: string;
@@ -38,45 +40,15 @@ const HelpModal: React.FC<HelpModalProps> = ({ title, content, className = '' })
</svg>
</button>
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{title}
</h3>
<button
onClick={() => setShowModal(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
aria-label={t('common.close')}
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">
{content}
</p>
<button
onClick={() => setShowModal(false)}
className="mt-4 w-full px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md transition-colors"
>
{t('common.close')}
</button>
</div>
</div>
)}
<ModalWrapper
isOpen={showModal}
onClose={() => setShowModal(false)}
title={title}
>
<p className="text-sm text-gray-600 dark:text-gray-300">
{content}
</p>
</ModalWrapper>
</>
);
};

View File

@@ -2,6 +2,7 @@ import QRCode from 'qrcode';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
import { MobileLoginErrorCode } from '@/entrypoints/popup/types/MobileLoginErrorCode';
import { MobileLoginUtility } from '@/entrypoints/popup/utils/MobileLoginUtility';
@@ -170,74 +171,50 @@ const MobileUnlockModal: React.FC<IMobileUnlockModalProps> = ({
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
if (!isOpen) {
return null;
}
const title = mode === 'unlock' ? t('auth.unlockWithMobile') : t('auth.loginWithMobile');
const description = t('auth.scanQrCode');
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div className="fixed inset-0 bg-black bg-opacity-80 transition-opacity" onClick={handleClose} />
<ModalWrapper
isOpen={isOpen}
onClose={handleClose}
title={title}
showHeaderBorder={false}
bodyClassName="px-6 pb-6"
>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{description}
</p>
{/* Modal */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-6 pb-6 pt-5 text-left shadow-xl transition-all w-full max-w-md">
{/* Close button */}
<button
type="button"
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
onClick={handleClose}
>
<span className="sr-only">{t('common.close')}</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{error && (
<div className="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 rounded text-red-700 dark:text-red-400 text-sm">
{getErrorMessage(error)}
</div>
)}
{/* Content */}
<div className="mt-3">
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
{title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{description}
</p>
{error && (
<div className="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 rounded text-red-700 dark:text-red-400 text-sm">
{getErrorMessage(error)}
</div>
)}
{qrCodeUrl && (
<div className="flex flex-col items-center mb-4">
<img src={qrCodeUrl} alt="QR Code" className="border-4 border-gray-200 dark:border-gray-600 rounded mb-3" />
<div className="text-gray-700 dark:text-gray-300 text-sm font-medium">
{formatTime(timeRemaining)}
</div>
</div>
)}
{!qrCodeUrl && !error && (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
)}
<button
type="button"
onClick={handleClose}
className="mt-4 w-full inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500"
>
{t('common.cancel')}
</button>
{qrCodeUrl && (
<div className="flex flex-col items-center mb-4">
<img src={qrCodeUrl} alt="QR Code" className="border-4 border-gray-200 dark:border-gray-600 rounded mb-3" />
<div className="text-gray-700 dark:text-gray-300 text-sm font-medium">
{formatTime(timeRemaining)}
</div>
</div>
</div>
</div>
)}
{!qrCodeUrl && !error && (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
)}
<button
type="button"
onClick={handleClose}
className="mt-4 w-full inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500"
>
{t('common.cancel')}
</button>
</ModalWrapper>
);
};

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
interface IModalProps {
isOpen: boolean;
@@ -14,6 +15,7 @@ interface IModalProps {
/**
* A reusable modal component for confirmations and alerts.
* Built on top of ModalWrapper for consistent behavior.
*/
const Modal: React.FC<IModalProps> = ({
isOpen,
@@ -25,81 +27,62 @@ const Modal: React.FC<IModalProps> = ({
cancelText = '',
variant = 'default'
}) => {
const { t } = useTranslation();
if (!isOpen) {
return null;
}
const confirmButtonClass = variant === 'danger'
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
: 'bg-primary-600 hover:bg-primary-700 focus:ring-primary-500';
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div className="fixed inset-0 bg-black bg-opacity-80 transition-opacity" onClick={onClose} />
{/* Modal */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all w-full max-w-lg">
{/* Close button */}
<button
type="button"
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
onClick={onClose}
>
<span className="sr-only">{t('common.close')}</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
<ModalWrapper
isOpen={isOpen}
onClose={onClose}
maxWidth="max-w-lg"
showHeaderBorder={false}
bodyClassName="px-4 pb-4 pt-5"
>
{/* Content */}
<div className="sm:flex sm:items-start">
{variant === 'danger' && (
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
<svg className="h-6 w-6 text-red-600 dark:text-red-200" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</button>
{/* Content */}
<div className="sm:flex sm:items-start">
{variant === 'danger' && (
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
<svg className="h-6 w-6 text-red-600 dark:text-red-200" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
)}
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3 className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
{title}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500 dark:text-gray-400">
{message}
</p>
</div>
</div>
</div>
{/* Actions */}
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
{confirmText && (
<button
type="button"
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${confirmButtonClass}`}
onClick={onConfirm}
>
{confirmText}
</button>
)}
{cancelText && (
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:mt-0 sm:w-auto"
onClick={onClose}
>
{cancelText}
</button>
)}
)}
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3 className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
{title}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500 dark:text-gray-400">
{message}
</p>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
{confirmText && (
<button
type="button"
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${confirmButtonClass}`}
onClick={onConfirm}
>
{confirmText}
</button>
)}
{cancelText && (
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:mt-0 sm:w-auto"
onClick={onClose}
>
{cancelText}
</button>
)}
</div>
</ModalWrapper>
);
};
export default Modal;
export default Modal;

View File

@@ -0,0 +1,134 @@
import React, { useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type ModalWrapperProps = {
isOpen: boolean;
onClose: () => void;
/** Modal title (optional - if not provided, no header is shown) */
title?: string;
children: React.ReactNode;
/** Optional max-width class (default: 'max-w-md') */
maxWidth?: string;
/** Whether to show the close button in header (default: true) */
showCloseButton?: boolean;
/** Optional footer actions */
footer?: React.ReactNode;
/** Whether to show header border (default: true) */
showHeaderBorder?: boolean;
/** Custom body padding class (default: 'px-6 py-4') */
bodyClassName?: string;
};
/**
* A generic modal wrapper component that provides consistent behavior:
* - Click outside to close (on backdrop)
* - Escape key to close
* - Dark overlay background
* - Consistent styling and animations
*/
const ModalWrapper: React.FC<ModalWrapperProps> = ({
isOpen,
onClose,
title,
children,
maxWidth = 'max-w-md',
showCloseButton = true,
footer,
showHeaderBorder = true,
bodyClassName = 'px-6 py-4'
}) => {
const { t } = useTranslation();
/**
* Handle escape key press to close modal.
*/
const handleKeyDown = useCallback((e: KeyboardEvent): void => {
if (e.key === 'Escape') {
onClose();
}
}, [onClose]);
/**
* Add/remove escape key listener when modal opens/closes.
*/
useEffect(() => {
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
return (): void => {
document.removeEventListener('keydown', handleKeyDown);
};
}
}, [isOpen, handleKeyDown]);
if (!isOpen) {
return null;
}
/**
* Handle click on the container (outside modal content) to close.
*/
const handleContainerClick = (e: React.MouseEvent<HTMLDivElement>): void => {
// Only close if clicking directly on the container, not the modal content
if (e.target === e.currentTarget) {
onClose();
}
};
const hasHeader = title || showCloseButton;
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black bg-opacity-80 transition-opacity"
onClick={onClose}
/>
{/* Modal container - clicking here (outside modal content) also closes */}
<div
className="fixed inset-0 flex items-center justify-center p-4"
onClick={handleContainerClick}
>
<div className={`relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow-xl transition-all w-full ${maxWidth} mx-4`}>
{/* Header */}
{hasHeader && (
<div className={`px-6 py-4 flex items-center justify-between ${showHeaderBorder ? 'border-b border-gray-200 dark:border-gray-700' : ''}`}>
{title && (
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{title}
</h2>
)}
{!title && <div />}
{showCloseButton && (
<button
type="button"
className="text-gray-400 hover:text-gray-500 focus:outline-none"
onClick={onClose}
>
<span className="sr-only">{t('common.close')}</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
)}
{/* Body */}
<div className={bodyClassName}>
{children}
</div>
{/* Footer */}
{footer && (
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
{footer}
</div>
)}
</div>
</div>
</div>
);
};
export default ModalWrapper;

View File

@@ -1,9 +1,11 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../Button';
import Button from '@/entrypoints/popup/components/Button';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
type PasskeyBypassDialogProps = {
isOpen: boolean;
origin: string;
onChoice: (choice: 'once' | 'always') => void;
onCancel: () => void;
@@ -13,6 +15,7 @@ type PasskeyBypassDialogProps = {
* Dialog for choosing how to bypass AliasVault passkey provider
*/
const PasskeyBypassDialog: React.FC<PasskeyBypassDialogProps> = ({
isOpen,
origin,
onChoice,
onCancel
@@ -20,40 +23,39 @@ const PasskeyBypassDialog: React.FC<PasskeyBypassDialogProps> = ({
const { t } = useTranslation();
return (
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
{t('passkeys.bypass.title')}
</h2>
<ModalWrapper
isOpen={isOpen}
onClose={onCancel}
title={t('passkeys.bypass.title')}
showCloseButton={true}
>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
{t('passkeys.bypass.description', { origin })}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
{t('passkeys.bypass.description', { origin })}
</p>
<div className="space-y-3">
<Button
variant="primary"
onClick={() => onChoice('once')}
>
{t('passkeys.bypass.thisTimeOnly')}
</Button>
<div className="space-y-3">
<Button
variant="primary"
onClick={() => onChoice('once')}
>
{t('passkeys.bypass.thisTimeOnly')}
</Button>
<Button
variant="secondary"
onClick={() => onChoice('always')}
>
{t('passkeys.bypass.alwaysForSite')}
</Button>
<Button
variant="secondary"
onClick={() => onChoice('always')}
>
{t('passkeys.bypass.alwaysForSite')}
</Button>
<Button
variant="secondary"
onClick={onCancel}
>
{t('common.cancel')}
</Button>
</div>
<Button
variant="secondary"
onClick={onCancel}
>
{t('common.cancel')}
</Button>
</div>
</div>
</ModalWrapper>
);
};

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
import type { PasswordSettings } from '@/utils/dist/core/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator';
@@ -61,158 +63,127 @@ const PasswordConfigDialog: React.FC<IPasswordConfigDialogProps> = ({
onClose();
}, [previewPassword, onSave, onClose]);
const handleCancel = useCallback(() => {
onClose();
}, [onClose]);
if (!isOpen) {
return null;
}
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div className="fixed inset-0 bg-black bg-opacity-60 transition-opacity" onClick={handleCancel} />
{/* Modal */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all w-full max-w-lg">
{/* Close button */}
<ModalWrapper
isOpen={isOpen}
onClose={onClose}
title={t('credentials.changePasswordComplexity')}
maxWidth="max-w-lg"
footer={
<div className="flex justify-end">
<button
type="button"
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
onClick={handleCancel}
className="inline-flex items-center gap-1 rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500"
onClick={handleSave}
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13l-3 3m0 0l-3-3m3 3V8m0 13a9 9 0 110-18 9 9 0 010 18z" />
</svg>
{t('common.use')}
</button>
</div>
}
>
<div className="space-y-4">
{/* Password Preview */}
<div>
<div className="flex items-center gap-2">
<input
type="text"
value={previewPassword}
readOnly
className="flex-1 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white font-mono"
/>
<button
type="button"
onClick={handleRefreshPreview}
className="px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
title={t('credentials.generateNewPreview')}
>
<svg className="w-4 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
</div>
</div>
{/* Modal content */}
<div className="sm:flex sm:items-start">
<div className="w-full mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-white mb-4">
{t('credentials.changePasswordComplexity')}
</h3>
{/* Character Type Toggle Buttons */}
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
{/* Lowercase Toggle */}
<button
type="button"
onClick={() => handleSettingChange('UseLowercase', !settings.UseLowercase)}
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
settings.UseLowercase
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
title={t('credentials.includeLowercase')}
>
<span className="font-mono text-base">a-z</span>
</button>
<div className="space-y-4">
{/* Password Preview */}
<div className="mt-4">
<div className="flex items-center gap-2">
<input
type="text"
value={previewPassword}
readOnly
className="flex-1 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white font-mono"
/>
<button
type="button"
onClick={handleRefreshPreview}
className="px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
title={t('credentials.generateNewPreview')}
>
<svg className="w-4 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
</div>
</div>
{/* Uppercase Toggle */}
<button
type="button"
onClick={() => handleSettingChange('UseUppercase', !settings.UseUppercase)}
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
settings.UseUppercase
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
title={t('credentials.includeUppercase')}
>
<span className="font-mono text-base">A-Z</span>
</button>
{/* Character Type Toggle Buttons */}
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
{/* Lowercase Toggle */}
<button
type="button"
onClick={() => handleSettingChange('UseLowercase', !settings.UseLowercase)}
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
settings.UseLowercase
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
title={t('credentials.includeLowercase')}
>
<span className="font-mono text-base">a-z</span>
</button>
{/* Numbers Toggle */}
<button
type="button"
onClick={() => handleSettingChange('UseNumbers', !settings.UseNumbers)}
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
settings.UseNumbers
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
title={t('credentials.includeNumbers')}
>
<span className="font-mono text-base">0-9</span>
</button>
{/* Uppercase Toggle */}
<button
type="button"
onClick={() => handleSettingChange('UseUppercase', !settings.UseUppercase)}
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
settings.UseUppercase
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
title={t('credentials.includeUppercase')}
>
<span className="font-mono text-base">A-Z</span>
</button>
{/* Special Characters Toggle */}
<button
type="button"
onClick={() => handleSettingChange('UseSpecialChars', !settings.UseSpecialChars)}
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
settings.UseSpecialChars
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
title={t('credentials.includeSpecialChars')}
>
<span className="font-mono text-base">!@#</span>
</button>
</div>
{/* Numbers Toggle */}
<button
type="button"
onClick={() => handleSettingChange('UseNumbers', !settings.UseNumbers)}
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
settings.UseNumbers
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
title={t('credentials.includeNumbers')}
>
<span className="font-mono text-base">0-9</span>
</button>
{/* Special Characters Toggle */}
<button
type="button"
onClick={() => handleSettingChange('UseSpecialChars', !settings.UseSpecialChars)}
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
settings.UseSpecialChars
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
title={t('credentials.includeSpecialChars')}
>
<span className="font-mono text-base">!@#</span>
</button>
</div>
{/* Avoid Ambiguous Characters - Checkbox */}
<div className="flex items-center">
<input
id="use-non-ambiguous"
type="checkbox"
checked={settings.UseNonAmbiguousChars}
onChange={(e) => handleSettingChange('UseNonAmbiguousChars', e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
/>
<label htmlFor="use-non-ambiguous" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
{t('credentials.avoidAmbiguousChars')}
</label>
</div>
</div>
</div>
{/* Action buttons */}
<div className="mt-5 sm:mt-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="inline-flex w-full justify-center items-center gap-1 rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 sm:ml-3 sm:w-auto"
onClick={handleSave}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13l-3 3m0 0l-3-3m3 3V8m0 13a9 9 0 110-18 9 9 0 010 18z" />
</svg>
{t('common.use')}
</button>
</div>
</div>
{/* Avoid Ambiguous Characters - Checkbox */}
<div className="flex items-center">
<input
id="use-non-ambiguous"
type="checkbox"
checked={settings.UseNonAmbiguousChars}
onChange={(e) => handleSettingChange('UseNonAmbiguousChars', e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
/>
<label htmlFor="use-non-ambiguous" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
{t('credentials.avoidAmbiguousChars')}
</label>
</div>
</div>
</div>
</div>
</ModalWrapper>
);
};
export default PasswordConfigDialog;
export default PasswordConfigDialog;

View File

@@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
type DeleteFolderModalProps = {
isOpen: boolean;
onClose: () => void;
@@ -55,112 +57,99 @@ const DeleteFolderModal: React.FC<DeleteFolderModalProps> = ({
};
/**
* Handle escape key press
* Handle close - only allow if not submitting
*/
const handleKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === 'Escape' && !isSubmitting) {
const handleClose = (): void => {
if (!isSubmitting) {
onClose();
}
};
if (!isOpen) {
return null;
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4"
onKeyDown={handleKeyDown}
>
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{t('items.deleteFolder')}
</h2>
</div>
{/* Body */}
<div className="px-6 py-4 space-y-4">
<p className="text-gray-700 dark:text-gray-300">
{t('items.deleteFolderConfirm', { folderName })}
</p>
{itemCount > 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('items.folderContainsItems', { count: itemCount })}
</p>
)}
{/* Option buttons */}
<div className="space-y-3 pt-2">
{/* Delete folder only - move items to root */}
<button
type="button"
onClick={handleDeleteFolderOnly}
disabled={isSubmitting}
className="w-full p-3 text-left border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
>
<div className="flex items-start gap-3">
<div className="mt-0.5 text-orange-500">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 14l2 2 4-4" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">
{t('items.deleteFolderKeepItems')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('items.deleteFolderKeepItemsDescription')}
</p>
</div>
</div>
</button>
{/* Delete folder and contents */}
{itemCount > 0 && (
<button
type="button"
onClick={handleDeleteFolderAndContents}
disabled={isSubmitting}
className="w-full p-3 text-left border border-red-300 dark:border-red-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50"
>
<div className="flex items-start gap-3">
<div className="mt-0.5 text-red-500">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
</div>
<div>
<p className="font-medium text-red-600 dark:text-red-400">
{t('items.deleteFolderAndItems')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('items.deleteFolderAndItemsDescription', { count: itemCount })}
</p>
</div>
</div>
</button>
)}
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<ModalWrapper
isOpen={isOpen}
onClose={handleClose}
title={t('items.deleteFolder')}
footer={
<div className="flex justify-end">
<button
type="button"
onClick={onClose}
onClick={handleClose}
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{t('common.cancel')}
</button>
</div>
}
>
<div className="space-y-4">
<p className="text-gray-700 dark:text-gray-300">
{t('items.deleteFolderConfirm', { folderName })}
</p>
{itemCount > 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('items.folderContainsItems', { count: itemCount })}
</p>
)}
{/* Option buttons */}
<div className="space-y-3 pt-2">
{/* Delete folder only - move items to root */}
<button
type="button"
onClick={handleDeleteFolderOnly}
disabled={isSubmitting}
className="w-full p-3 text-left border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
>
<div className="flex items-start gap-3">
<div className="mt-0.5 text-orange-500">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 14l2 2 4-4" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">
{t('items.deleteFolderKeepItems')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('items.deleteFolderKeepItemsDescription')}
</p>
</div>
</div>
</button>
{/* Delete folder and contents */}
{itemCount > 0 && (
<button
type="button"
onClick={handleDeleteFolderAndContents}
disabled={isSubmitting}
className="w-full p-3 text-left border border-red-300 dark:border-red-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50"
>
<div className="flex items-start gap-3">
<div className="mt-0.5 text-red-500">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
</div>
<div>
<p className="font-medium text-red-600 dark:text-red-400">
{t('items.deleteFolderAndItems')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('items.deleteFolderAndItemsDescription', { count: itemCount })}
</p>
</div>
</div>
</button>
)}
</div>
</div>
</div>
</ModalWrapper>
);
};

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
type FolderModalProps = {
isOpen: boolean;
onClose: () => void;
@@ -57,81 +59,58 @@ const FolderModal: React.FC<FolderModalProps> = ({
}
};
/**
* Handle the escape key press.
*/
const handleKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === 'Escape') {
onClose();
}
};
if (!isOpen) {
return null;
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4"
onKeyDown={handleKeyDown}
>
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{mode === 'create' ? t('items.createFolder') : t('items.editFolder')}
</h2>
<ModalWrapper
isOpen={isOpen}
onClose={onClose}
title={mode === 'create' ? t('items.createFolder') : t('items.editFolder')}
footer={
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onClose}
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{t('common.cancel')}
</button>
<button
type="submit"
form="folder-form"
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-white bg-orange-600 hover:bg-orange-700 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50"
>
{isSubmitting
? t('common.saving')
: mode === 'create'
? t('common.create')
: t('common.save')
}
</button>
</div>
{/* Body */}
<form onSubmit={handleSubmit}>
<div className="px-6 py-4">
<label
htmlFor="folderName"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('items.folderName')}
</label>
<input
id="folderName"
type="text"
value={folderName}
onChange={(e) => setFolderName(e.target.value)}
placeholder={t('items.folderNamePlaceholder')}
autoFocus
className="w-full p-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-blue-500 focus:border-blue-500"
/>
{error && (
<p className="mt-2 text-sm text-red-600 dark:text-red-400">{error}</p>
)}
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
<button
type="button"
onClick={onClose}
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{t('common.cancel')}
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-white bg-orange-600 hover:bg-orange-700 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50"
>
{isSubmitting
? t('common.saving')
: mode === 'create'
? t('common.create')
: t('common.save')
}
</button>
</div>
</form>
</div>
</div>
}
>
<form id="folder-form" onSubmit={handleSubmit}>
<label
htmlFor="folderName"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('items.folderName')}
</label>
<input
id="folderName"
type="text"
value={folderName}
onChange={(e) => setFolderName(e.target.value)}
placeholder={t('items.folderNamePlaceholder')}
autoFocus
className="w-full p-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-blue-500 focus:border-blue-500"
/>
{error && (
<p className="mt-2 text-sm text-red-600 dark:text-red-400">{error}</p>
)}
</form>
</ModalWrapper>
);
};

View File

@@ -1,6 +1,8 @@
import React, { useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
import type { FieldType } from '@/utils/dist/core/models/vault';
/**
@@ -204,8 +206,9 @@ const AddFieldMenu: React.FC<AddFieldMenuProps> = ({
{/* Dropdown Menu */}
{isOpen && (
<>
{/* Dark overlay backdrop for better visibility */}
<div
className="fixed inset-0 z-10"
className="fixed inset-0 z-10 bg-black bg-opacity-50"
onClick={() => setIsOpen(false)}
/>
<div className="absolute bottom-full left-0 right-0 mb-1 z-20 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg overflow-hidden">
@@ -239,69 +242,66 @@ const AddFieldMenu: React.FC<AddFieldMenuProps> = ({
</div>
{/* Custom Field Modal */}
{showCustomFieldModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96 max-w-full mx-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t('itemTypes.addCustomField')}
</h3>
<ModalWrapper
isOpen={showCustomFieldModal}
onClose={handleCloseCustomFieldModal}
title={t('itemTypes.addCustomField')}
footer={
<div className="flex gap-3">
<button
type="button"
onClick={handleAddCustomField}
disabled={!customFieldLabel.trim()}
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{t('common.add')}
</button>
<button
type="button"
onClick={handleCloseCustomFieldModal}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
{t('common.cancel')}
</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('itemTypes.fieldLabel')}
</label>
<input
type="text"
value={customFieldLabel}
onChange={(e) => setCustomFieldLabel(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
placeholder={t('itemTypes.enterFieldName')}
autoFocus
/>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('itemTypes.fieldLabel')}
</label>
<input
type="text"
value={customFieldLabel}
onChange={(e) => setCustomFieldLabel(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
placeholder={t('itemTypes.enterFieldName')}
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('itemTypes.fieldType')}
</label>
<select
value={customFieldType}
onChange={(e) => setCustomFieldType(e.target.value as FieldType)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
>
<option value="Text">{t('itemTypes.fieldTypes.text')}</option>
<option value="Hidden">{t('itemTypes.fieldTypes.hidden')}</option>
<option value="Email">{t('itemTypes.fieldTypes.email')}</option>
<option value="URL">{t('itemTypes.fieldTypes.url')}</option>
<option value="Phone">{t('itemTypes.fieldTypes.phone')}</option>
<option value="Number">{t('itemTypes.fieldTypes.number')}</option>
<option value="Date">{t('itemTypes.fieldTypes.date')}</option>
<option value="TextArea">{t('itemTypes.fieldTypes.textArea')}</option>
</select>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
type="button"
onClick={handleAddCustomField}
disabled={!customFieldLabel.trim()}
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{t('common.add')}
</button>
<button
type="button"
onClick={handleCloseCustomFieldModal}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
{t('common.cancel')}
</button>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('itemTypes.fieldType')}
</label>
<select
value={customFieldType}
onChange={(e) => setCustomFieldType(e.target.value as FieldType)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
>
<option value="Text">{t('itemTypes.fieldTypes.text')}</option>
<option value="Hidden">{t('itemTypes.fieldTypes.hidden')}</option>
<option value="Email">{t('itemTypes.fieldTypes.email')}</option>
<option value="URL">{t('itemTypes.fieldTypes.url')}</option>
<option value="Phone">{t('itemTypes.fieldTypes.phone')}</option>
<option value="Number">{t('itemTypes.fieldTypes.number')}</option>
<option value="Date">{t('itemTypes.fieldTypes.date')}</option>
<option value="TextArea">{t('itemTypes.fieldTypes.textArea')}</option>
</select>
</div>
</div>
)}
</ModalWrapper>
</>
);
};

View File

@@ -661,8 +661,8 @@ const CredentialAddEdit: React.FC = () => {
setShowDeleteModal(false);
void handleDelete();
}}
title={t('credentials.deleteCredentialTitle')}
message={t('credentials.deleteCredentialConfirm')}
title={t('credentials.deleteItemTitle')}
message={t('credentials.deleteItemConfirm')}
confirmText={t('common.delete')}
cancelText={t('common.cancel')}
variant="danger"

View File

@@ -1387,8 +1387,8 @@ const ItemAddEdit: React.FC = () => {
<Modal
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
title={t('credentials.deleteCredentialTitle')}
message={t('credentials.deleteCredentialConfirmation')}
title={t('credentials.deleteItemTitle')}
message={t('credentials.deleteItemConfirm')}
confirmText={t('common.delete')}
cancelText={t('common.cancel')}
onConfirm={handleDelete}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ConfirmDeleteModal from '@/entrypoints/popup/components/Dialogs/ConfirmDeleteModal';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
@@ -70,19 +71,19 @@ const RecentlyDeleted: React.FC = () => {
/**
* Permanently delete an item.
*/
const handlePermanentDelete = useCallback(async (itemId: string) => {
if (!dbContext?.sqliteClient) {
const handlePermanentDelete = useCallback(async () => {
if (!dbContext?.sqliteClient || !selectedItemId) {
return;
}
await executeVaultMutationAsync(async () => {
await dbContext.sqliteClient!.permanentlyDeleteItem(itemId);
await dbContext.sqliteClient!.permanentlyDeleteItem(selectedItemId);
});
loadItems();
setShowConfirmDelete(false);
setSelectedItemId(null);
}, [dbContext?.sqliteClient, executeVaultMutationAsync, loadItems]);
}, [dbContext?.sqliteClient, executeVaultMutationAsync, loadItems, selectedItemId]);
/**
* Empty all items from Recently Deleted (permanent delete all).
@@ -124,6 +125,14 @@ const RecentlyDeleted: React.FC = () => {
load();
}, [dbContext?.sqliteClient, setIsLoading, loadItems]);
/**
* Handle closing the delete confirmation modal.
*/
const handleCloseDeleteModal = useCallback(() => {
setShowConfirmDelete(false);
setSelectedItemId(null);
}, []);
if (isLoading) {
return (
<div className="flex justify-center items-center p-8">
@@ -221,63 +230,24 @@ const RecentlyDeleted: React.FC = () => {
)}
{/* Confirm Delete Modal */}
{showConfirmDelete && selectedItemId && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm mx-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{t('recentlyDeleted.confirmDeleteTitle')}
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-4">
{t('recentlyDeleted.confirmDeleteMessage')}
</p>
<div className="flex justify-end gap-2">
<button
onClick={() => {
setShowConfirmDelete(false);
setSelectedItemId(null);
}}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
{t('common.cancel')}
</button>
<button
onClick={() => handlePermanentDelete(selectedItemId)}
className="px-4 py-2 text-sm bg-red-600 text-white rounded hover:bg-red-700"
>
{t('recentlyDeleted.deletePermanently')}
</button>
</div>
</div>
</div>
)}
<ConfirmDeleteModal
isOpen={showConfirmDelete && !!selectedItemId}
onClose={handleCloseDeleteModal}
onConfirm={handlePermanentDelete}
title={t('recentlyDeleted.confirmDeleteTitle')}
message={t('recentlyDeleted.confirmDeleteMessage')}
confirmText={t('recentlyDeleted.deletePermanently')}
/>
{/* Confirm Empty All Modal */}
{showConfirmEmptyAll && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm mx-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{t('recentlyDeleted.confirmEmptyAllTitle')}
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-4">
{t('recentlyDeleted.confirmEmptyAllMessage', { count: items.length })}
</p>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowConfirmEmptyAll(false)}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
{t('common.cancel')}
</button>
<button
onClick={handleEmptyAll}
className="px-4 py-2 text-sm bg-red-600 text-white rounded hover:bg-red-700"
>
{t('recentlyDeleted.emptyAll')}
</button>
</div>
</div>
</div>
)}
<ConfirmDeleteModal
isOpen={showConfirmEmptyAll}
onClose={() => setShowConfirmEmptyAll(false)}
onConfirm={handleEmptyAll}
title={t('recentlyDeleted.confirmEmptyAllTitle')}
message={t('recentlyDeleted.confirmEmptyAllMessage', { count: items.length })}
confirmText={t('recentlyDeleted.emptyAll')}
/>
</div>
);
};

View File

@@ -365,13 +365,12 @@ const PasskeyAuthenticate: React.FC = () => {
return (
<>
{showBypassDialog && request && (
<PasskeyBypassDialog
origin={new URL(request.origin).hostname}
onChoice={handleBypassChoice}
onCancel={() => setShowBypassDialog(false)}
/>
)}
<PasskeyBypassDialog
isOpen={showBypassDialog && !!request}
origin={request ? new URL(request.origin).hostname : ''}
onChoice={handleBypassChoice}
onCancel={() => setShowBypassDialog(false)}
/>
<div className="space-y-6">
<div className="text-center">

View File

@@ -546,13 +546,12 @@ const PasskeyCreate: React.FC = () => {
return (
<>
{showBypassDialog && request && (
<PasskeyBypassDialog
origin={new URL(request.origin).hostname}
onChoice={handleBypassChoice}
onCancel={() => setShowBypassDialog(false)}
/>
)}
<PasskeyBypassDialog
isOpen={showBypassDialog && !!request}
origin={request ? new URL(request.origin).hostname : ''}
onChoice={handleBypassChoice}
onCancel={() => setShowBypassDialog(false)}
/>
{localLoading && (
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">

View File

@@ -190,7 +190,7 @@
"title": "Credentials",
"addCredential": "Add Credential",
"editCredential": "Edit Credential",
"deleteCredential": "Delete Credential",
"deleteCredential": "Delete Item",
"credentialDetails": "Credential Details",
"serviceName": "Service Name",
"itemName": "Name",
@@ -208,9 +208,8 @@
"createdAt": "Created",
"updatedAt": "Last updated",
"saveCredential": "Save credential",
"deleteCredentialTitle": "Delete Credential",
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
"deleteCredentialConfirmation": "Are you sure you want to delete this item? This action cannot be undone.",
"deleteItemTitle": "Delete Item",
"deleteItemConfirm": "Are you sure you want to delete this item? This action cannot be undone.",
"filters": {
"all": "(All) Credentials",
"passkeys": "Passkeys",