mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
30ccaf51a8
* feat(ui): add reusable Modal component Portal + dimmed backdrop (no backdrop-filter — on WebKitGTK the blur bleeds onto modal content on some GPU stacks), Escape/backdrop/X to close, header (icon/title/subtitle) + scrollable body + footer slot. First consumer is the update modal; the other hand-rolled modals migrate onto it over time. * fix(updater): rebuild the update modal on the reusable Modal Fixes a user report (Manjaro / AMD / X11): blurry modal text (the eq-popup backdrop-filter bled onto the content), the version arrow sitting between the two header rows, and the unclear Skip / Remind buttons. Now uses the no-blur Modal shell; the header icon aligns with the title; Skip is a clear button and Remind me later is the accent action when there is no in-app install. Drops the now-dead update-modal shell CSS. * docs(changelog): update notification popup fix (PR #1142)
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
import { type ReactNode, useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { X } from 'lucide-react';
|
|
|
|
interface ModalProps {
|
|
open: boolean;
|
|
/** Called on Escape, backdrop click, and the header close button. */
|
|
onClose: () => void;
|
|
title: ReactNode;
|
|
/** Smaller line under the title (e.g. a version range). */
|
|
subtitle?: ReactNode;
|
|
/** Leading icon, aligned with the title line. */
|
|
icon?: ReactNode;
|
|
/** Action row pinned to the bottom, above the modal edge. */
|
|
footer?: ReactNode;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
/** Hide the header close (X) button. */
|
|
hideClose?: boolean;
|
|
/** Accessible label / tooltip for the close button. */
|
|
closeLabel?: string;
|
|
children: ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Reusable modal shell: a portal to `document.body` with an opaque, centered
|
|
* card over a dimmed backdrop. Closes on Escape, backdrop click and the header
|
|
* X. Deliberately uses **no `backdrop-filter`** — on WebKitGTK (some GPU stacks)
|
|
* the blur bleeds onto the modal content, making text look unfocused. The other
|
|
* hand-rolled modals migrate onto this over time.
|
|
*/
|
|
export default function Modal({
|
|
open, onClose, title, subtitle, icon, footer, size = 'md', hideClose, closeLabel, children,
|
|
}: ModalProps) {
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [open, onClose]);
|
|
|
|
if (!open) return null;
|
|
|
|
return createPortal(
|
|
<div className="ui-modal-backdrop" onClick={onClose}>
|
|
<div
|
|
className={`ui-modal ui-modal--${size}`}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<div className="ui-modal-header">
|
|
{icon && <span className="ui-modal-icon">{icon}</span>}
|
|
<div className="ui-modal-titles">
|
|
<span className="ui-modal-title">{title}</span>
|
|
{subtitle != null && <span className="ui-modal-subtitle">{subtitle}</span>}
|
|
</div>
|
|
{!hideClose && (
|
|
<button
|
|
type="button"
|
|
className="ui-modal-close"
|
|
onClick={onClose}
|
|
aria-label={closeLabel ?? 'Close'}
|
|
data-tooltip={closeLabel}
|
|
data-tooltip-pos="bottom"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="ui-modal-body">{children}</div>
|
|
{footer && <div className="ui-modal-footer">{footer}</div>}
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|