Initial Commit
This commit is contained in:
75
src/tui/components/Button.tsx
Normal file
75
src/tui/components/Button.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Button component with focus styling.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text, useFocus } from 'ink';
|
||||
import { colors } from '../theme.js';
|
||||
|
||||
/**
|
||||
* Props for the Button component.
|
||||
*/
|
||||
interface ButtonProps {
|
||||
/** Button label */
|
||||
label: string;
|
||||
/** Whether button is focused */
|
||||
focused?: boolean;
|
||||
/** Whether button is disabled */
|
||||
disabled?: boolean;
|
||||
/** Optional keyboard shortcut hint */
|
||||
shortcut?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button component with focus highlighting.
|
||||
*/
|
||||
export function Button({
|
||||
label,
|
||||
focused = false,
|
||||
disabled = false,
|
||||
shortcut,
|
||||
}: ButtonProps): React.ReactElement {
|
||||
const bgColor = disabled
|
||||
? colors.textMuted
|
||||
: focused
|
||||
? colors.focus
|
||||
: colors.secondary;
|
||||
|
||||
const textColor = disabled
|
||||
? colors.bg
|
||||
: focused
|
||||
? colors.bg
|
||||
: colors.text;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box paddingX={1} marginRight={1}>
|
||||
<Text
|
||||
backgroundColor={bgColor}
|
||||
color={textColor}
|
||||
bold={focused}
|
||||
>
|
||||
{` ${label} `}
|
||||
</Text>
|
||||
</Box>
|
||||
{shortcut && (
|
||||
<Text color={colors.textMuted} dimColor>({shortcut})</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Button row component for multiple buttons.
|
||||
*/
|
||||
interface ButtonRowProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ButtonRow({ children }: ButtonRowProps): React.ReactElement {
|
||||
return (
|
||||
<Box marginTop={1} gap={2}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
109
src/tui/components/Input.tsx
Normal file
109
src/tui/components/Input.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Text input component with focus styling.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import TextInput from 'ink-text-input';
|
||||
import { colors } from '../theme.js';
|
||||
|
||||
/**
|
||||
* Props for the Input component.
|
||||
*/
|
||||
interface InputProps {
|
||||
/** Current value */
|
||||
value: string;
|
||||
/** Change handler */
|
||||
onChange: (value: string) => void;
|
||||
/** Submit handler (Enter key) */
|
||||
onSubmit?: (value: string) => void;
|
||||
/** Placeholder text */
|
||||
placeholder?: string;
|
||||
/** Label shown above input */
|
||||
label?: string;
|
||||
/** Whether input is focused */
|
||||
focus?: boolean;
|
||||
/** Whether to mask input (for passwords) */
|
||||
mask?: string;
|
||||
/** Whether input is disabled */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text input component with label and focus styling.
|
||||
*/
|
||||
export function Input({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
placeholder,
|
||||
label,
|
||||
focus = true,
|
||||
mask,
|
||||
disabled = false,
|
||||
}: InputProps): React.ReactElement {
|
||||
const borderColor = focus ? colors.focus : colors.border;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{label && (
|
||||
<Text color={colors.text} bold>{label}</Text>
|
||||
)}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={borderColor}
|
||||
paddingX={1}
|
||||
>
|
||||
{disabled ? (
|
||||
<Text color={colors.textMuted}>{value || placeholder || ''}</Text>
|
||||
) : (
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
placeholder={placeholder}
|
||||
focus={focus}
|
||||
mask={mask}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-line text display (read-only, styled like input).
|
||||
*/
|
||||
interface TextDisplayProps {
|
||||
/** Text content */
|
||||
content: string;
|
||||
/** Label shown above */
|
||||
label?: string;
|
||||
/** Whether to show border */
|
||||
border?: boolean;
|
||||
}
|
||||
|
||||
export function TextDisplay({
|
||||
content,
|
||||
label,
|
||||
border = true
|
||||
}: TextDisplayProps): React.ReactElement {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{label && (
|
||||
<Text color={colors.text} bold>{label}</Text>
|
||||
)}
|
||||
{border ? (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={colors.border}
|
||||
paddingX={1}
|
||||
>
|
||||
<Text>{content}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Text>{content}</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
159
src/tui/components/List.tsx
Normal file
159
src/tui/components/List.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Selectable list component with keyboard navigation.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { colors } from '../theme.js';
|
||||
|
||||
/**
|
||||
* List item type.
|
||||
*/
|
||||
export interface ListItem<T = unknown> {
|
||||
/** Unique key for the item */
|
||||
key: string;
|
||||
/** Display label */
|
||||
label: string;
|
||||
/** Optional secondary text */
|
||||
description?: string;
|
||||
/** Optional value associated with item */
|
||||
value?: T;
|
||||
/** Whether item is disabled */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the List component.
|
||||
*/
|
||||
interface ListProps<T> {
|
||||
/** List items */
|
||||
items: ListItem<T>[];
|
||||
/** Currently selected index */
|
||||
selectedIndex: number;
|
||||
/** Selection change handler */
|
||||
onSelect: (index: number) => void;
|
||||
/** Item activation handler (Enter key) */
|
||||
onActivate?: (item: ListItem<T>, index: number) => void;
|
||||
/** Whether list is focused */
|
||||
focus?: boolean;
|
||||
/** Maximum visible items (for scrolling) */
|
||||
maxVisible?: number;
|
||||
/** Optional label */
|
||||
label?: string;
|
||||
/** Show border */
|
||||
border?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selectable list with keyboard navigation.
|
||||
*/
|
||||
export function List<T>({
|
||||
items,
|
||||
selectedIndex,
|
||||
onSelect,
|
||||
onActivate,
|
||||
focus = true,
|
||||
maxVisible = 10,
|
||||
label,
|
||||
border = true,
|
||||
}: ListProps<T>): React.ReactElement {
|
||||
// Handle keyboard input
|
||||
useInput((input, key) => {
|
||||
if (!focus) return;
|
||||
|
||||
if (key.upArrow || input === 'k') {
|
||||
const newIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1;
|
||||
onSelect(newIndex);
|
||||
} else if (key.downArrow || input === 'j') {
|
||||
const newIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0;
|
||||
onSelect(newIndex);
|
||||
} else if (key.return && onActivate && items[selectedIndex]) {
|
||||
const item = items[selectedIndex];
|
||||
if (item && !item.disabled) {
|
||||
onActivate(item, selectedIndex);
|
||||
}
|
||||
}
|
||||
}, { isActive: focus });
|
||||
|
||||
// Calculate visible range for scrolling
|
||||
const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), items.length - maxVisible));
|
||||
const visibleItems = items.slice(startIndex, startIndex + maxVisible);
|
||||
|
||||
const borderColor = focus ? colors.focus : colors.border;
|
||||
|
||||
const content = (
|
||||
<Box flexDirection="column">
|
||||
{visibleItems.map((item, visibleIndex) => {
|
||||
const actualIndex = startIndex + visibleIndex;
|
||||
const isSelected = actualIndex === selectedIndex;
|
||||
|
||||
return (
|
||||
<Box key={item.key}>
|
||||
<Text
|
||||
color={item.disabled ? colors.textMuted : isSelected ? colors.bg : colors.text}
|
||||
backgroundColor={isSelected ? colors.focus : undefined}
|
||||
bold={isSelected}
|
||||
dimColor={item.disabled}
|
||||
>
|
||||
{isSelected ? '▸ ' : ' '}
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.description && (
|
||||
<Text color={colors.textMuted} dimColor> - {item.description}</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{items.length === 0 && (
|
||||
<Text color={colors.textMuted} dimColor>No items</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{label && <Text color={colors.text} bold>{label}</Text>}
|
||||
{border ? (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={borderColor}
|
||||
paddingX={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
{content}
|
||||
</Box>
|
||||
) : content}
|
||||
{items.length > maxVisible && (
|
||||
<Text color={colors.textMuted} dimColor>
|
||||
{startIndex + 1}-{Math.min(startIndex + maxVisible, items.length)} of {items.length}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple inline list for displaying items without selection.
|
||||
*/
|
||||
interface SimpleListProps {
|
||||
items: string[];
|
||||
label?: string;
|
||||
bullet?: string;
|
||||
}
|
||||
|
||||
export function SimpleList({
|
||||
items,
|
||||
label,
|
||||
bullet = '•'
|
||||
}: SimpleListProps): React.ReactElement {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{label && <Text color={colors.text} bold>{label}</Text>}
|
||||
{items.map((item, index) => (
|
||||
<Text key={index} color={colors.text}>
|
||||
{bullet} {item}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
130
src/tui/components/ProgressBar.tsx
Normal file
130
src/tui/components/ProgressBar.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Progress bar and step indicator components.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { colors } from '../theme.js';
|
||||
|
||||
/**
|
||||
* Props for ProgressBar component.
|
||||
*/
|
||||
interface ProgressBarProps {
|
||||
/** Current progress (0-100) */
|
||||
percent: number;
|
||||
/** Bar width in characters */
|
||||
width?: number;
|
||||
/** Show percentage text */
|
||||
showPercent?: boolean;
|
||||
/** Bar character */
|
||||
character?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple progress bar component.
|
||||
*/
|
||||
export function ProgressBar({
|
||||
percent,
|
||||
width = 40,
|
||||
showPercent = true,
|
||||
character = '█',
|
||||
}: ProgressBarProps): React.ReactElement {
|
||||
const clampedPercent = Math.max(0, Math.min(100, percent));
|
||||
const filled = Math.round((clampedPercent / 100) * width);
|
||||
const empty = width - filled;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={colors.success}>{character.repeat(filled)}</Text>
|
||||
<Text color={colors.textMuted} dimColor>{'░'.repeat(empty)}</Text>
|
||||
{showPercent && (
|
||||
<Text color={colors.text}> {Math.round(clampedPercent)}%</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step definition for StepIndicator.
|
||||
*/
|
||||
export interface Step {
|
||||
/** Step label */
|
||||
label: string;
|
||||
/** Whether step is completed */
|
||||
completed?: boolean;
|
||||
/** Whether step is current */
|
||||
current?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for StepIndicator component.
|
||||
*/
|
||||
interface StepIndicatorProps {
|
||||
/** Steps to display */
|
||||
steps: Step[];
|
||||
/** Current step index (0-based) */
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step indicator showing progress through a multi-step wizard.
|
||||
*/
|
||||
export function StepIndicator({
|
||||
steps,
|
||||
currentStep,
|
||||
}: StepIndicatorProps): React.ReactElement {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box gap={1}>
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = index < currentStep;
|
||||
const isCurrent = index === currentStep;
|
||||
|
||||
const color = isCompleted ? colors.success :
|
||||
isCurrent ? colors.focus :
|
||||
colors.textMuted;
|
||||
|
||||
const icon = isCompleted ? '✓' :
|
||||
isCurrent ? '▸' :
|
||||
'○';
|
||||
|
||||
return (
|
||||
<Box key={index}>
|
||||
<Text color={color} bold={isCurrent}>
|
||||
{icon} {step.label}
|
||||
</Text>
|
||||
{index < steps.length - 1 && (
|
||||
<Text color={colors.textMuted}> → </Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Text color={colors.textMuted}>
|
||||
Step {currentStep + 1} of {steps.length}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading spinner with optional message.
|
||||
*/
|
||||
interface LoadingProps {
|
||||
/** Loading message */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function Loading({ message = 'Loading...' }: LoadingProps): React.ReactElement {
|
||||
// Simple spinner using Ink's spinner component
|
||||
const Spinner = require('ink-spinner').default;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={colors.primary}>
|
||||
<Spinner type="dots" />
|
||||
</Text>
|
||||
<Text color={colors.text}> {message}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
71
src/tui/components/Screen.tsx
Normal file
71
src/tui/components/Screen.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Screen wrapper component providing consistent layout.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { colors } from '../theme.js';
|
||||
|
||||
/**
|
||||
* Props for the Screen component.
|
||||
*/
|
||||
interface ScreenProps {
|
||||
/** Screen title displayed in header */
|
||||
title: string;
|
||||
/** Optional subtitle */
|
||||
subtitle?: string;
|
||||
/** Screen content */
|
||||
children: ReactNode;
|
||||
/** Optional footer content */
|
||||
footer?: ReactNode;
|
||||
/** Optional help text shown at bottom */
|
||||
helpText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Screen wrapper component.
|
||||
* Provides consistent header, content area, and footer layout.
|
||||
*/
|
||||
export function Screen({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
footer,
|
||||
helpText
|
||||
}: ScreenProps): React.ReactElement {
|
||||
return (
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{/* Header */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={colors.primary}
|
||||
paddingX={1}
|
||||
marginBottom={1}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text color={colors.primary} bold>{title}</Text>
|
||||
{subtitle && <Text color={colors.textMuted}>{subtitle}</Text>}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
{/* Help text */}
|
||||
{helpText && (
|
||||
<Box paddingX={1} marginTop={1}>
|
||||
<Text color={colors.textMuted} dimColor>{helpText}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
{footer && (
|
||||
<Box paddingX={1} marginTop={1}>
|
||||
{footer}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
10
src/tui/components/index.ts
Normal file
10
src/tui/components/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Export all shared components.
|
||||
*/
|
||||
|
||||
export { Screen } from './Screen.js';
|
||||
export { Input, TextDisplay } from './Input.js';
|
||||
export { Button, ButtonRow } from './Button.js';
|
||||
export { List, SimpleList, type ListItem } from './List.js';
|
||||
export { InputDialog, ConfirmDialog, MessageDialog } from './Dialog.js';
|
||||
export { ProgressBar, StepIndicator, Loading, type Step } from './ProgressBar.js';
|
||||
Reference in New Issue
Block a user