Files
bambuddy/frontend/src/components/ConfirmModal.tsx
T
maziggy 1d344a8536 fix(ui): resolve light-theme low-contrast semantic text app-wide (#1909)
The app was built dark-first, so hundreds of hardcoded Tailwind semantic
text/icon utilities at light shades (text-amber-400, text-blue-300, ...) had
no dark: variant. With darkMode:'class' they applied in light theme too,
producing washed-out text on pale tints and white cards — including the three
reported spots (AMS Drying banner, Archives no-3MF warning, debug-logging
banner). Give each a theme-aware pair: a darker readable shade in light theme
with the original pinned to dark:, so dark theme is unchanged. ~100 files.

The bambu-* CSS-variable palette (self-correcting) and the dark-only SpoolBuddy
kiosk are left untouched. Plain text-white is already theme-aware via the
existing index.css .text-white override, so it needed no changes.
2026-07-07 09:54:10 +02:00

130 lines
4.3 KiB
TypeScript

import { useEffect, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTriangle, Loader2 } from 'lucide-react';
import { Card, CardContent } from './Card';
import { Button } from './Button';
interface ConfirmModalProps {
title: string;
message: string;
confirmText?: string;
cancelText?: string;
cancelVariant?: 'primary' | 'secondary' | 'danger' | 'ghost';
cardClassName?: string;
// Tailwind z-index utility applied to the fixed overlay. Defaults to
// ``z-50``. Use a higher value (e.g. ``z-[110]``) when this confirm
// dialog is rendered from inside another modal that uses ``z-[100]`` —
// without it the confirm dialog sits behind its parent (#1336 follow-up).
overlayZIndex?: string;
variant?: 'danger' | 'warning' | 'default';
isLoading?: boolean;
loadingText?: string;
// Disable the confirm button without a loading spinner. Used when an
// external precondition forbids the action (e.g. #1734 — a related queue
// item is mid-print, so the archive delete must be blocked at the UI
// layer too even though the backend will 409 anyway).
confirmDisabled?: boolean;
// Optional extra content rendered between the message and the buttons —
// used for opt-in checkboxes (e.g. the "Also remove from statistics"
// toggle in the archive delete confirmation, #1343).
children?: ReactNode;
onConfirm: () => void;
onCancel: () => void;
}
export function ConfirmModal({
title,
message,
confirmText,
cancelText,
cancelVariant,
cardClassName,
overlayZIndex,
variant = 'default',
isLoading = false,
loadingText,
confirmDisabled = false,
children,
onConfirm,
onCancel,
}: ConfirmModalProps) {
const { t } = useTranslation();
const resolvedConfirmText = confirmText ?? t('common.confirm');
const resolvedCancelText = cancelText ?? t('common.cancel');
const resolvedLoadingText = loadingText ?? t('common.loading');
// Close on Escape key (but not while loading)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && !isLoading) onCancel();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onCancel, isLoading]);
const variantStyles = {
danger: {
icon: 'text-red-600 dark:text-red-400',
button: 'bg-red-500 hover:bg-red-600',
},
warning: {
icon: 'text-yellow-600 dark:text-yellow-400',
button: 'bg-yellow-500 hover:bg-yellow-600 text-black',
},
default: {
icon: 'text-bambu-green',
button: 'bg-bambu-green hover:bg-bambu-green-dark',
},
};
const styles = variantStyles[variant];
return (
<div
className={`fixed inset-0 bg-black/50 flex items-center justify-center p-4 ${overlayZIndex ?? 'z-50'}`}
onClick={isLoading ? undefined : onCancel}
>
<Card
className={`w-full max-w-md ${cardClassName ?? ''}`}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
>
<CardContent className="p-6">
<div className="flex items-start gap-4">
<div className={`p-2 rounded-full bg-bambu-dark ${styles.icon}`}>
<AlertTriangle className="w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white mb-2">{title}</h3>
<p className="text-bambu-gray text-sm whitespace-pre-line">{message}</p>
{children && <div className="mt-4">{children}</div>}
</div>
</div>
<div className="flex gap-3 mt-6">
<Button
variant={cancelVariant ?? 'secondary'}
onClick={onCancel}
className="flex-1"
disabled={isLoading}
>
{resolvedCancelText}
</Button>
<Button
onClick={onConfirm}
className={`flex-1 ${styles.button}`}
disabled={isLoading || confirmDisabled}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{resolvedLoadingText}
</>
) : (
resolvedConfirmText
)}
</Button>
</div>
</CardContent>
</Card>
</div>
);
}