feat(users): per-user library assignment + themed confirm modal (#222)

Adds Navidrome library assignment to the User Management settings
panel: GET /api/library + PUT /api/user/{id}/library wired through
new Rust commands. UserForm gets a checkbox picker (hidden for
admins, who auto-receive all libraries server-side) with inline
validation. User rows compacted to a single clickable line with
hover highlight; native confirm() replaced by a portal-based
ConfirmModal (theme-aware, ESC/Enter, vertically centered).

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frank Stellmacher
2026-04-20 22:18:10 +02:00
committed by GitHub
parent 6d3c50264a
commit 459c9f688d
13 changed files with 392 additions and 66 deletions
+74
View File
@@ -0,0 +1,74 @@
import { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { X } from 'lucide-react';
interface ConfirmModalProps {
open: boolean;
title: string;
message: string;
confirmLabel: string;
cancelLabel: string;
danger?: boolean;
onConfirm: () => void;
onCancel: () => void;
}
export default function ConfirmModal({
open,
title,
message,
confirmLabel,
cancelLabel,
danger,
onConfirm,
onCancel,
}: ConfirmModalProps) {
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onCancel();
else if (e.key === 'Enter') onConfirm();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onCancel, onConfirm]);
if (!open) return null;
const confirmStyle = danger
? { background: 'var(--danger)', borderColor: 'var(--danger)', color: '#fff' }
: undefined;
return createPortal(
<div
className="modal-overlay"
onClick={onCancel}
role="dialog"
aria-modal="true"
style={{ alignItems: 'center', paddingTop: 0 }}
>
<div
className="modal-content"
onClick={e => e.stopPropagation()}
style={{ maxWidth: '380px' }}
>
<button className="modal-close" onClick={onCancel} aria-label={cancelLabel}>
<X size={18} />
</button>
<h3 style={{ marginBottom: '0.5rem', fontFamily: 'var(--font-display)' }}>{title}</h3>
<p style={{ color: 'var(--text-secondary)', marginBottom: '1.25rem', lineHeight: 1.5 }}>
{message}
</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button className="btn btn-ghost" onClick={onCancel} autoFocus>
{cancelLabel}
</button>
<button className="btn btn-primary" style={confirmStyle} onClick={onConfirm}>
{confirmLabel}
</button>
</div>
</div>
</div>,
document.body,
);
}