Initial Commit
This commit is contained in:
243
src/tui/components/Dialog.tsx
Normal file
243
src/tui/components/Dialog.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Dialog components for modals, confirmations, and input dialogs.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import TextInput from 'ink-text-input';
|
||||
import { colors } from '../theme.js';
|
||||
|
||||
/**
|
||||
* Base dialog wrapper props.
|
||||
*/
|
||||
interface DialogWrapperProps {
|
||||
/** Dialog title */
|
||||
title: string;
|
||||
/** Border color */
|
||||
borderColor?: string;
|
||||
/** Dialog content */
|
||||
children: React.ReactNode;
|
||||
/** Dialog width */
|
||||
width?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base dialog wrapper component.
|
||||
*/
|
||||
function DialogWrapper({
|
||||
title,
|
||||
borderColor = colors.primary,
|
||||
children,
|
||||
width = 60,
|
||||
}: DialogWrapperProps): React.ReactElement {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="double"
|
||||
borderColor={borderColor}
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
width={width}
|
||||
>
|
||||
<Text color={borderColor} bold>{title}</Text>
|
||||
<Box marginY={1} flexDirection="column">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for InputDialog component.
|
||||
*/
|
||||
interface InputDialogProps {
|
||||
/** Dialog title */
|
||||
title: string;
|
||||
/** Input prompt/label */
|
||||
prompt: string;
|
||||
/** Initial value */
|
||||
initialValue?: string;
|
||||
/** Placeholder text */
|
||||
placeholder?: string;
|
||||
/** Submit handler */
|
||||
onSubmit: (value: string) => void;
|
||||
/** Cancel handler */
|
||||
onCancel: () => void;
|
||||
/** Whether dialog is visible/active */
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input dialog for getting text input from user.
|
||||
*/
|
||||
export function InputDialog({
|
||||
title,
|
||||
prompt,
|
||||
initialValue = '',
|
||||
placeholder,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
isActive = true,
|
||||
}: InputDialogProps): React.ReactElement {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (!isActive) return;
|
||||
|
||||
if (key.escape) {
|
||||
onCancel();
|
||||
}
|
||||
}, { isActive });
|
||||
|
||||
const handleSubmit = (val: string) => {
|
||||
onSubmit(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogWrapper title={title} borderColor={colors.primary}>
|
||||
<Text>{prompt}</Text>
|
||||
<Box marginTop={1} borderStyle="single" borderColor={colors.focus} paddingX={1}>
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder={placeholder}
|
||||
focus={isActive}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.textMuted}>Enter to submit • Esc to cancel</Text>
|
||||
</Box>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for ConfirmDialog component.
|
||||
*/
|
||||
interface ConfirmDialogProps {
|
||||
/** Dialog title */
|
||||
title: string;
|
||||
/** Confirmation message */
|
||||
message: string;
|
||||
/** Confirm handler */
|
||||
onConfirm: () => void;
|
||||
/** Cancel handler */
|
||||
onCancel: () => void;
|
||||
/** Whether dialog is visible/active */
|
||||
isActive?: boolean;
|
||||
/** Confirm button label */
|
||||
confirmLabel?: string;
|
||||
/** Cancel button label */
|
||||
cancelLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation dialog with Yes/No options.
|
||||
*/
|
||||
export function ConfirmDialog({
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
isActive = true,
|
||||
confirmLabel = 'Yes',
|
||||
cancelLabel = 'No',
|
||||
}: ConfirmDialogProps): React.ReactElement {
|
||||
const [selected, setSelected] = useState<'confirm' | 'cancel'>('confirm');
|
||||
|
||||
useInput((input, key) => {
|
||||
if (!isActive) return;
|
||||
|
||||
if (key.leftArrow || key.rightArrow || key.tab) {
|
||||
setSelected(prev => prev === 'confirm' ? 'cancel' : 'confirm');
|
||||
} else if (key.return) {
|
||||
if (selected === 'confirm') {
|
||||
onConfirm();
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
} else if (key.escape || input === 'n' || input === 'N') {
|
||||
onCancel();
|
||||
} else if (input === 'y' || input === 'Y') {
|
||||
onConfirm();
|
||||
}
|
||||
}, { isActive });
|
||||
|
||||
return (
|
||||
<DialogWrapper title={title} borderColor={colors.warning}>
|
||||
<Text wrap="wrap">{message}</Text>
|
||||
<Box marginTop={1} gap={2}>
|
||||
<Text
|
||||
backgroundColor={selected === 'confirm' ? colors.focus : colors.secondary}
|
||||
color={selected === 'confirm' ? colors.bg : colors.text}
|
||||
bold={selected === 'confirm'}
|
||||
>
|
||||
{` ${confirmLabel} `}
|
||||
</Text>
|
||||
<Text
|
||||
backgroundColor={selected === 'cancel' ? colors.focus : colors.secondary}
|
||||
color={selected === 'cancel' ? colors.bg : colors.text}
|
||||
bold={selected === 'cancel'}
|
||||
>
|
||||
{` ${cancelLabel} `}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.textMuted}>Y/N or Tab to switch • Enter to select</Text>
|
||||
</Box>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for MessageDialog component.
|
||||
*/
|
||||
interface MessageDialogProps {
|
||||
/** Dialog title */
|
||||
title: string;
|
||||
/** Message content */
|
||||
message: string;
|
||||
/** Close handler */
|
||||
onClose: () => void;
|
||||
/** Dialog type for styling */
|
||||
type?: 'info' | 'error' | 'success';
|
||||
/** Whether dialog is visible/active */
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple message dialog (info, error, success).
|
||||
*/
|
||||
export function MessageDialog({
|
||||
title,
|
||||
message,
|
||||
onClose,
|
||||
type = 'info',
|
||||
isActive = true,
|
||||
}: MessageDialogProps): React.ReactElement {
|
||||
useInput((input, key) => {
|
||||
if (!isActive) return;
|
||||
|
||||
if (key.return || key.escape) {
|
||||
onClose();
|
||||
}
|
||||
}, { isActive });
|
||||
|
||||
const borderColor = type === 'error' ? colors.error :
|
||||
type === 'success' ? colors.success :
|
||||
colors.info;
|
||||
|
||||
const icon = type === 'error' ? '✗' :
|
||||
type === 'success' ? '✓' :
|
||||
'ℹ';
|
||||
|
||||
return (
|
||||
<DialogWrapper title={`${icon} ${title}`} borderColor={borderColor}>
|
||||
<Text wrap="wrap">{message}</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.textMuted}>Press Enter or Esc to close</Text>
|
||||
</Box>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user