Initial Commit

This commit is contained in:
2026-01-29 07:13:33 +00:00
commit 399e93f714
34 changed files with 7663 additions and 0 deletions

204
src/tui/App.tsx Normal file
View 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>
);
}