ui-nyla/src/hooks/usePrompt.tsx
ValueOn AG 71bf6baae5 fixes
2026-03-30 23:39:13 +02:00

161 lines
4.9 KiB
TypeScript

/**
* usePrompt — application-level prompt dialog replacing native browser prompt().
*
* Usage:
* const { prompt, PromptDialog } = usePrompt();
* const value = await prompt('Bitte Namen eingeben:', { title: 'Umbenennen' });
* if (value !== null) { ... }
* // Render <PromptDialog /> once in the component tree.
*/
import React, { useState, useCallback, useRef } from 'react';
export interface PromptOptions {
title?: string;
confirmLabel?: string;
cancelLabel?: string;
placeholder?: string;
defaultValue?: string;
variant?: 'primary' | 'danger';
}
interface PromptState {
message: string;
options: Required<PromptOptions>;
resolve: (value: string | null) => void;
}
const _defaults: Required<PromptOptions> = {
title: 'Eingabe',
confirmLabel: 'OK',
cancelLabel: 'Abbrechen',
placeholder: '',
defaultValue: '',
variant: 'primary',
};
export function usePrompt() {
const [state, setState] = useState<PromptState | null>(null);
const resolveRef = useRef<((v: string | null) => void) | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const prompt = useCallback((message: string, options?: PromptOptions): Promise<string | null> => {
return new Promise<string | null>((resolve) => {
resolveRef.current = resolve;
setState({
message,
options: { ..._defaults, ...options },
resolve,
});
});
}, []);
const _handleConfirm = useCallback(() => {
const val = inputRef.current?.value ?? '';
resolveRef.current?.(val);
resolveRef.current = null;
setState(null);
}, []);
const _handleCancel = useCallback(() => {
resolveRef.current?.(null);
resolveRef.current = null;
setState(null);
}, []);
const PromptDialog: React.FC = useCallback(() => {
if (!state) return null;
const { message, options } = state;
const isDanger = options.variant === 'danger';
return (
<div
onClick={_handleCancel}
style={{
position: 'fixed', inset: 0, zIndex: 10000,
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(2px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: 'var(--surface-color, #1a1a2e)',
border: '1px solid var(--border-color, var(--color-border, #333))',
borderRadius: '12px',
padding: '1.5rem',
minWidth: 360, maxWidth: 500,
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
display: 'flex', flexDirection: 'column', gap: '1.25rem',
}}
>
<h3 style={{
margin: 0, fontSize: '1.05rem', fontWeight: 600,
color: 'var(--text-primary, #e0e0e0)',
}}>
{options.title}
</h3>
<p style={{
margin: 0, fontSize: '0.9rem', lineHeight: 1.5,
color: 'var(--text-secondary, #999)',
}}>
{message}
</p>
<input
ref={inputRef}
autoFocus
defaultValue={options.defaultValue}
placeholder={options.placeholder}
onKeyDown={(e) => {
if (e.key === 'Enter') _handleConfirm();
if (e.key === 'Escape') _handleCancel();
}}
style={{
padding: '10px 14px',
borderRadius: '8px',
border: '1px solid var(--border-color, var(--color-border, #ccc))',
background: 'var(--input-bg, var(--bg-primary, #ffffff))',
color: 'var(--text-primary, #1a1a1a)',
fontSize: '0.9rem',
outline: 'none',
width: '100%',
boxSizing: 'border-box',
}}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
<button
onClick={_handleCancel}
style={{
padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 500,
border: '1px solid var(--color-border, #444)',
background: 'transparent',
color: 'var(--text-secondary, #aaa)',
cursor: 'pointer',
}}
>
{options.cancelLabel}
</button>
<button
onClick={_handleConfirm}
style={{
padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 600,
border: 'none',
background: isDanger ? '#ef4444' : 'var(--color-primary, #3b82f6)',
color: '#fff',
cursor: 'pointer',
}}
>
{options.confirmLabel}
</button>
</div>
</div>
</div>
);
}, [state, _handleConfirm, _handleCancel]);
return { prompt, PromptDialog };
}