Initial Commit
This commit is contained in:
204
src/tui/App.tsx
Normal file
204
src/tui/App.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Main App component for the XO Wallet CLI.
|
||||
* Uses Ink for terminal rendering with React components.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text, useApp, useInput } from 'ink';
|
||||
import { NavigationProvider, useNavigation } from './hooks/useNavigation.js';
|
||||
import { AppProvider, useAppContext, useDialog, useStatus } from './hooks/useAppContext.js';
|
||||
import type { WalletController } from '../controllers/wallet-controller.js';
|
||||
import type { InvitationController } from '../controllers/invitation-controller.js';
|
||||
import { colors, logoSmall } from './theme.js';
|
||||
|
||||
// Screen imports (will be created)
|
||||
import { SeedInputScreen } from './screens/SeedInput.js';
|
||||
import { WalletStateScreen } from './screens/WalletState.js';
|
||||
import { TemplateListScreen } from './screens/TemplateList.js';
|
||||
import { ActionWizardScreen } from './screens/ActionWizard.js';
|
||||
import { InvitationScreen } from './screens/Invitation.js';
|
||||
import { TransactionScreen } from './screens/Transaction.js';
|
||||
|
||||
/**
|
||||
* Props for the App component.
|
||||
*/
|
||||
interface AppProps {
|
||||
walletController: WalletController;
|
||||
invitationController: InvitationController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Router component that renders the current screen.
|
||||
*/
|
||||
function Router(): React.ReactElement {
|
||||
const { screen } = useNavigation();
|
||||
|
||||
switch (screen) {
|
||||
case 'seed-input':
|
||||
return <SeedInputScreen />;
|
||||
case 'wallet':
|
||||
return <WalletStateScreen />;
|
||||
case 'templates':
|
||||
return <TemplateListScreen />;
|
||||
case 'wizard':
|
||||
return <ActionWizardScreen />;
|
||||
case 'invitations':
|
||||
return <InvitationScreen />;
|
||||
case 'transaction':
|
||||
return <TransactionScreen />;
|
||||
default:
|
||||
return <Text color={colors.error}>Unknown screen: {screen}</Text>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Status bar component shown at the bottom of the screen.
|
||||
*/
|
||||
function StatusBar(): React.ReactElement {
|
||||
const { status } = useStatus();
|
||||
const { screen, canGoBack } = useNavigation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={colors.border}
|
||||
paddingX={1}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text color={colors.primary} bold>{logoSmall}</Text>
|
||||
<Text color={colors.textMuted}>{status}</Text>
|
||||
<Text color={colors.textMuted}>
|
||||
{canGoBack ? 'ESC: Back | ' : ''}q: Quit
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog overlay component for modals.
|
||||
*/
|
||||
function DialogOverlay(): React.ReactElement | null {
|
||||
const { dialog, setDialog } = useDialog();
|
||||
|
||||
useInput((input, key) => {
|
||||
if (!dialog?.visible) return;
|
||||
|
||||
if (key.return || input === 'y' || input === 'Y') {
|
||||
if (dialog.type === 'confirm' && dialog.onConfirm) {
|
||||
dialog.onConfirm();
|
||||
} else {
|
||||
dialog.onCancel?.();
|
||||
}
|
||||
} else if (key.escape || input === 'n' || input === 'N') {
|
||||
dialog.onCancel?.();
|
||||
}
|
||||
}, { isActive: dialog?.visible ?? false });
|
||||
|
||||
if (!dialog?.visible) return null;
|
||||
|
||||
const borderColor = dialog.type === 'error' ? colors.error :
|
||||
dialog.type === 'confirm' ? colors.warning :
|
||||
colors.info;
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="double"
|
||||
borderColor={borderColor}
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
width={60}
|
||||
>
|
||||
<Text color={borderColor} bold>
|
||||
{dialog.type === 'error' ? '✗ Error' :
|
||||
dialog.type === 'confirm' ? '? Confirm' :
|
||||
'ℹ Info'}
|
||||
</Text>
|
||||
<Box marginY={1}>
|
||||
<Text wrap="wrap">{dialog.message}</Text>
|
||||
</Box>
|
||||
<Text color={colors.textMuted}>
|
||||
{dialog.type === 'confirm'
|
||||
? 'Press Y to confirm, N or ESC to cancel'
|
||||
: 'Press Enter or ESC to close'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main content wrapper with global keybindings.
|
||||
*/
|
||||
function MainContent(): React.ReactElement {
|
||||
const { exit } = useApp();
|
||||
const { goBack, canGoBack } = useNavigation();
|
||||
const { dialog } = useDialog();
|
||||
const appContext = useAppContext();
|
||||
|
||||
// Global keybindings (disabled when dialog is shown)
|
||||
useInput((input, key) => {
|
||||
// Don't handle global keys when dialog is shown
|
||||
if (dialog?.visible) return;
|
||||
|
||||
// Quit on 'q' or Ctrl+C
|
||||
if (input === 'q' || (key.ctrl && input === 'c')) {
|
||||
appContext.exit();
|
||||
exit();
|
||||
}
|
||||
|
||||
// Go back on Escape
|
||||
if (key.escape && canGoBack) {
|
||||
goBack();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" height="100%">
|
||||
{/* Main content area */}
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<Router />
|
||||
</Box>
|
||||
|
||||
{/* Status bar */}
|
||||
<StatusBar />
|
||||
|
||||
{/* Dialog overlay */}
|
||||
<DialogOverlay />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main App component.
|
||||
* Sets up providers and renders the main content.
|
||||
*/
|
||||
export function App({ walletController, invitationController }: AppProps): React.ReactElement {
|
||||
const { exit } = useApp();
|
||||
|
||||
const handleExit = () => {
|
||||
// Cleanup controllers if needed
|
||||
walletController.stop();
|
||||
exit();
|
||||
};
|
||||
|
||||
return (
|
||||
<AppProvider
|
||||
walletController={walletController}
|
||||
invitationController={invitationController}
|
||||
onExit={handleExit}
|
||||
>
|
||||
<NavigationProvider initialScreen="seed-input">
|
||||
<MainContent />
|
||||
</NavigationProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user