Fix dialog focus
This commit is contained in:
@@ -6,11 +6,12 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import TextInput from '../components/TextInput.js';
|
||||
import { Button } from '../components/Button.js';
|
||||
import { useNavigation } from '../hooks/useNavigation.js';
|
||||
import { useAppContext, useStatus } from '../hooks/useAppContext.js';
|
||||
import { useBlockableInput } from '../hooks/useInputLayer.js';
|
||||
import { colors, logo } from '../theme.js';
|
||||
|
||||
import fs from 'fs';
|
||||
@@ -170,7 +171,7 @@ export function SeedInputScreen(): React.ReactElement {
|
||||
}, [mnemonicFiles, doInitialize]);
|
||||
|
||||
// Keyboard navigation
|
||||
useInput((input, key) => {
|
||||
useBlockableInput((_input, key) => {
|
||||
if (isSubmitting) return;
|
||||
|
||||
// Tab / Shift-Tab to cycle focus sections
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ScrollableList, type ListItemData } from '../components/List.js';
|
||||
import { useNavigation } from '../hooks/useNavigation.js';
|
||||
import { useAppContext, useStatus } from '../hooks/useAppContext.js';
|
||||
import { useBlockableInput } from '../hooks/useInputLayer.js';
|
||||
import { colors, logoSmall } from '../theme.js';
|
||||
|
||||
// XO Imports
|
||||
@@ -258,8 +259,7 @@ export function TemplateListScreen(): React.ReactElement {
|
||||
}, [currentTemplate, navigate]);
|
||||
|
||||
// Handle keyboard navigation
|
||||
useInput((input, key) => {
|
||||
// Tab to switch panels
|
||||
useBlockableInput((_input, key) => {
|
||||
if (key.tab) {
|
||||
setFocusedPanel(prev => prev === 'templates' ? 'actions' : 'templates');
|
||||
return;
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ConfirmDialog } from '../components/Dialog.js';
|
||||
import { useNavigation } from '../hooks/useNavigation.js';
|
||||
import { useAppContext, useStatus } from '../hooks/useAppContext.js';
|
||||
import { useBlockableInput } from '../hooks/useInputLayer.js';
|
||||
import { useInvitation } from '../hooks/useInvitations.js';
|
||||
import { colors, logoSmall, formatSatoshis, formatHex } from '../theme.js';
|
||||
import { copyToClipboard } from '../utils/clipboard.js';
|
||||
@@ -147,10 +148,8 @@ export function TransactionScreen(): React.ReactElement {
|
||||
}
|
||||
}, [signTransaction, copyTransactionHex, goBack]);
|
||||
|
||||
// Handle keyboard navigation
|
||||
useInput((input, key) => {
|
||||
if (showBroadcastConfirm) return;
|
||||
|
||||
// Handle keyboard navigation — automatically blocked when the confirm dialog is open.
|
||||
useBlockableInput((input, key) => {
|
||||
// Tab to switch panels
|
||||
if (key.tab) {
|
||||
setFocusedPanel(prev => {
|
||||
@@ -177,7 +176,7 @@ export function TransactionScreen(): React.ReactElement {
|
||||
handleAction(action.value);
|
||||
}
|
||||
}
|
||||
}, { isActive: !showBroadcastConfirm });
|
||||
});
|
||||
|
||||
// Extract transaction data from invitation
|
||||
const commits = invitation?.commits ?? [];
|
||||
@@ -407,7 +406,6 @@ export function TransactionScreen(): React.ReactElement {
|
||||
message='Are you sure you want to broadcast this transaction? This action cannot be undone.'
|
||||
onConfirm={broadcastTransaction}
|
||||
onCancel={() => setShowBroadcastConfirm(false)}
|
||||
isActive={showBroadcastConfirm}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ScrollableList, type ListItemData } from '../components/List.js';
|
||||
import { QRCode } from '../components/QRCode.js';
|
||||
import { useNavigation } from '../hooks/useNavigation.js';
|
||||
import { useAppContext, useStatus, useDialog } from '../hooks/useAppContext.js';
|
||||
import { useAppContext, useStatus } from '../hooks/useAppContext.js';
|
||||
import { useInputLayer, useLayeredInput, useBlockableInput, useIsInputCaptured } from '../hooks/useInputLayer.js';
|
||||
import { colors, formatSatoshis, formatHex, logoSmall } from '../theme.js';
|
||||
import type { HistoryItem } from '../../services/history.js';
|
||||
|
||||
@@ -65,6 +66,39 @@ const menuItems: ListItemData<string>[] = [
|
||||
*/
|
||||
type HistoryListItem = ListItemData<HistoryDisplayRow>;
|
||||
|
||||
/**
|
||||
* QR code dialog overlay — auto-captures input via the layer system.
|
||||
* Rendered only while a QR address is visible; closes on Enter/Esc.
|
||||
*/
|
||||
function QRDialogOverlay({ address, onClose }: { address: string; onClose: () => void }): React.ReactElement {
|
||||
useInputLayer('qr-dialog');
|
||||
|
||||
useLayeredInput('qr-dialog', (_input, key) => {
|
||||
if (key.escape || key.return) {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<QRCode
|
||||
value={address}
|
||||
dialog
|
||||
dialogTitle="Receive Address"
|
||||
showValue
|
||||
/>
|
||||
<Box justifyContent="center" marginTop={1}>
|
||||
<Text color={colors.textMuted}>Press Enter or Esc to close</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet State Screen Component.
|
||||
* Displays wallet balance, history, and action menu.
|
||||
@@ -73,7 +107,6 @@ export function WalletStateScreen(): React.ReactElement {
|
||||
const { navigate } = useNavigation();
|
||||
const { appService, showError, showInfo } = useAppContext();
|
||||
const { setStatus } = useStatus();
|
||||
const { setDialog } = useDialog();
|
||||
|
||||
// State
|
||||
const [balance, setBalance] = useState<{ totalSatoshis: bigint; utxoCount: number } | null>(null);
|
||||
@@ -169,7 +202,6 @@ export function WalletStateScreen(): React.ReactElement {
|
||||
console.log(result);
|
||||
|
||||
setQrAddress(result.address);
|
||||
setDialog({ visible: true, type: 'custom', message: '' });
|
||||
setStatus('Address generated');
|
||||
|
||||
// Refresh to show updated state
|
||||
@@ -227,16 +259,10 @@ export function WalletStateScreen(): React.ReactElement {
|
||||
});
|
||||
}, [history]);
|
||||
|
||||
// Handle keyboard navigation between panels and QR dialog dismissal
|
||||
useInput((input, key) => {
|
||||
if (qrAddress) {
|
||||
if (key.escape || key.return) {
|
||||
setQrAddress(null);
|
||||
setDialog(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Screen input — automatically blocked when any dialog/overlay is capturing.
|
||||
const isCaptured = useIsInputCaptured();
|
||||
|
||||
useBlockableInput((_input, key) => {
|
||||
if (key.tab) {
|
||||
setFocusedPanel(prev => prev === 'menu' ? 'history' : 'menu');
|
||||
}
|
||||
@@ -383,7 +409,7 @@ export function WalletStateScreen(): React.ReactElement {
|
||||
selectedIndex={selectedMenuIndex}
|
||||
onSelect={setSelectedMenuIndex}
|
||||
onActivate={handleMenuItemActivate}
|
||||
focus={focusedPanel === 'menu'}
|
||||
focus={focusedPanel === 'menu' && !isCaptured}
|
||||
emptyMessage="No actions"
|
||||
/>
|
||||
</Box>
|
||||
@@ -411,7 +437,7 @@ export function WalletStateScreen(): React.ReactElement {
|
||||
items={historyListItems}
|
||||
selectedIndex={selectedHistoryIndex}
|
||||
onSelect={setSelectedHistoryIndex}
|
||||
focus={focusedPanel === 'history'}
|
||||
focus={focusedPanel === 'history' && !isCaptured}
|
||||
maxVisible={10}
|
||||
emptyMessage="No history found"
|
||||
renderItem={renderHistoryItem}
|
||||
@@ -429,22 +455,10 @@ export function WalletStateScreen(): React.ReactElement {
|
||||
|
||||
{/* QR Code dialog overlay for generated addresses */}
|
||||
{qrAddress && (
|
||||
<Box
|
||||
position="absolute"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<QRCode
|
||||
value={qrAddress}
|
||||
dialog
|
||||
dialogTitle="Receive Address"
|
||||
showValue
|
||||
/>
|
||||
<Box justifyContent="center" marginTop={1}>
|
||||
<Text color={colors.textMuted}>Press Enter or Esc to close</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<QRDialogOverlay
|
||||
address={qrAddress}
|
||||
onClose={() => setQrAddress(null)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useInput } from 'ink';
|
||||
import { useBlockableInput } from '../../../hooks/useInputLayer.js';
|
||||
import type { ActionWizardState } from './useActionWizard.js';
|
||||
|
||||
/**
|
||||
@@ -9,7 +9,7 @@ import type { ActionWizardState } from './useActionWizard.js';
|
||||
* component to keep it purely presentational.
|
||||
*/
|
||||
export function useWizardKeyboard(wizard: ActionWizardState): void {
|
||||
useInput(
|
||||
useBlockableInput(
|
||||
(input, key) => {
|
||||
// ── Tab: cycle through content and button bar ─────────
|
||||
if (key.tab) {
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { InputDialog } from '../../components/Dialog.js';
|
||||
import { ScrollableList, type ListItemData, type ListGroup } from '../../components/List.js';
|
||||
import { useNavigation } from '../../hooks/useNavigation.js';
|
||||
import { useAppContext, useStatus } from '../../hooks/useAppContext.js';
|
||||
import { useBlockableInput, useIsInputCaptured } from '../../hooks/useInputLayer.js';
|
||||
import { useInvitations } from '../../hooks/useInvitations.js';
|
||||
import { colors, logoSmall, formatSatoshis } from '../../theme.js';
|
||||
import { copyToClipboard } from '../../utils/clipboard.js';
|
||||
@@ -421,10 +422,10 @@ export function InvitationScreen(): React.ReactElement {
|
||||
}, [handleAction]);
|
||||
|
||||
// ── Keyboard navigation ──────────────────────────────────────────────────
|
||||
// Disabled when the ID dialog or import flow is open.
|
||||
const isOverlayOpen = showIdDialog || importingId !== null;
|
||||
// Automatically blocked when any dialog/overlay is capturing input.
|
||||
const isCaptured = useIsInputCaptured();
|
||||
|
||||
useInput((input, key) => {
|
||||
useBlockableInput((input, key) => {
|
||||
if (key.tab) {
|
||||
setFocusedPanel(prev => prev === 'list' ? 'actions' : 'list');
|
||||
return;
|
||||
@@ -437,7 +438,7 @@ export function InvitationScreen(): React.ReactElement {
|
||||
if (input === 'i') {
|
||||
setShowIdDialog(true);
|
||||
}
|
||||
}, { isActive: !isOverlayOpen });
|
||||
});
|
||||
|
||||
// ── Render helpers ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -639,7 +640,7 @@ export function InvitationScreen(): React.ReactElement {
|
||||
selectedIndex={selectedIndex}
|
||||
onSelect={setSelectedIndex}
|
||||
onActivate={handleListItemActivate}
|
||||
focus={focusedPanel === 'list' && !isOverlayOpen}
|
||||
focus={focusedPanel === 'list' && !isCaptured}
|
||||
maxVisible={6}
|
||||
groups={invitationListGroups}
|
||||
emptyMessage="No invitations yet"
|
||||
@@ -663,7 +664,7 @@ export function InvitationScreen(): React.ReactElement {
|
||||
selectedIndex={selectedActionIndex}
|
||||
onSelect={setSelectedActionIndex}
|
||||
onActivate={handleActionItemActivate}
|
||||
focus={focusedPanel === 'actions' && !isOverlayOpen}
|
||||
focus={focusedPanel === 'actions' && !isCaptured}
|
||||
emptyMessage="No actions"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { colors, logoSmall } from '../../../theme.js';
|
||||
import { StepIndicator, type Step } from '../../../components/ProgressBar.js';
|
||||
|
||||
@@ -24,6 +24,7 @@ import { IMPORT_STEPS, type ImportFlowProps, type SelectableUTXO } from './types
|
||||
import type { Invitation } from '../../../../services/invitation.js';
|
||||
import type { XOTemplate } from '@xo-cash/types';
|
||||
import { DialogWrapper } from '../../../components/Dialog.js';
|
||||
import { useInputLayer, useLayeredInput } from '../../../hooks/useInputLayer.js';
|
||||
import { InvitationBuilder } from '@xo-cash/engine';
|
||||
import { hexToBin } from '@bitauth/libauth';
|
||||
|
||||
@@ -140,18 +141,27 @@ export function InvitationImportFlow({
|
||||
return (raw && typeof raw === 'object' && 'name' in raw) ? String(raw.name) : selectedRole;
|
||||
})();
|
||||
|
||||
showInfo(
|
||||
`Invitation imported and accepted!\n\n` +
|
||||
`Role: ${roleName}\n` +
|
||||
`Template: ${template?.name ?? invitation?.data.templateIdentifier ?? 'Unknown'}\n` +
|
||||
`Action: ${invitation?.data.actionIdentifier ?? 'Unknown'}`
|
||||
);
|
||||
setStatus('Ready');
|
||||
onClose();
|
||||
}, [selectedRole, template, invitation, showInfo, setStatus, onClose]);
|
||||
|
||||
// ── Keyboard handling for FetchStep error retry ──────────────────────────
|
||||
// FetchStep auto-advances on success but shows error state with retry on failure.
|
||||
useInput((_input, key) => {
|
||||
// ── Keyboard handling ────────────────────────────────────────────────────
|
||||
// The import flow registers its own layer so it captures input above the
|
||||
// parent screen. Individual steps also register sub-layers when needed.
|
||||
useInputLayer('import-flow');
|
||||
|
||||
useLayeredInput('import-flow', (_input, key) => {
|
||||
if (currentStep !== 0) return;
|
||||
// Enter retries, Esc cancels — handled within FetchStep rendering,
|
||||
// but we also catch Esc here for safety.
|
||||
if (key.escape) handleCancel();
|
||||
}, { isActive: currentStep === 0 });
|
||||
});
|
||||
|
||||
// ── Step router ──────────────────────────────────────────────────────────
|
||||
const renderStep = (): React.ReactNode => {
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { colors, formatSatoshis } from '../../../../theme.js';
|
||||
import { useLayeredInput } from '../../../../hooks/useInputLayer.js';
|
||||
import type { InputsSelectStepProps, SelectableUTXO } from '../types.js';
|
||||
import { autoSelectGreedyUtxos, mapUnspentOutputsToSelectable } from '../../../../../utils/invitation-flow.js';
|
||||
import type { UnspentOutputData } from '@xo-cash/state';
|
||||
@@ -98,6 +99,7 @@ export function InputsSelectStep({
|
||||
const utxoIdToSuitableResource = new Map<string, UnspentOutputData>();
|
||||
for (const outputIdentifier of outputIdentifiers) {
|
||||
const suitableResources = await invitation.findSuitableResources({
|
||||
|
||||
outputIdentifier,
|
||||
});
|
||||
console.log('suitableResources', outputIdentifier, JSON.stringify(suitableResources, null, 2));
|
||||
@@ -141,25 +143,19 @@ export function InputsSelectStep({
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Keyboard handling
|
||||
useInput((input, key) => {
|
||||
if (!isActive) return;
|
||||
|
||||
// Keyboard handling — gated by the import-flow layer so dialogs on top block input.
|
||||
useLayeredInput('import-flow', (input, key) => {
|
||||
if (key.upArrow || input === 'k') {
|
||||
setFocusedIndex(prev => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow || input === 'j') {
|
||||
setFocusedIndex(prev => Math.min(utxos.length - 1, prev + 1));
|
||||
} else if (input === ' ' || (key.return && utxos.length > 0)) {
|
||||
// Space or Enter toggles the focused UTXO
|
||||
if (utxos.length > 0) toggleSelection(focusedIndex);
|
||||
} else if (input === 'a') {
|
||||
// Select all
|
||||
setUtxos(prev => prev.map(u => ({ ...u, selected: true })));
|
||||
} else if (input === 'n') {
|
||||
// Deselect all
|
||||
setUtxos(prev => prev.map(u => ({ ...u, selected: false })));
|
||||
} else if (key.tab) {
|
||||
// Tab confirms selection (moves to next step)
|
||||
if (hasEnough) {
|
||||
onComplete(utxos.filter(u => u.selected));
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { colors, formatSatoshis } from '../../../../theme.js';
|
||||
import { useLayeredInput } from '../../../../hooks/useInputLayer.js';
|
||||
import {
|
||||
getInvitationState,
|
||||
getStateColorName,
|
||||
@@ -40,8 +41,7 @@ export function PreviewInvitationStep({
|
||||
onCancel,
|
||||
isActive,
|
||||
}: PreviewStepProps): React.ReactElement {
|
||||
useInput((_input, key) => {
|
||||
if (!isActive) return;
|
||||
useLayeredInput('import-flow', (_input, key) => {
|
||||
if (key.return) onComplete();
|
||||
if (key.escape) onCancel();
|
||||
}, { isActive });
|
||||
@@ -62,87 +62,129 @@ export function PreviewInvitationStep({
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{/* Template & action info */}
|
||||
{/* Template info */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={colors.primary} bold>Template: </Text>
|
||||
<Text color={colors.text}>{template?.name ?? invitation.data.templateIdentifier}</Text>
|
||||
<Box>
|
||||
<Text color={colors.primary} bold>Template:</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={colors.text}>{template?.name ?? invitation.data.templateIdentifier}</Text>
|
||||
</Box>
|
||||
{template?.description && (
|
||||
<Text color={colors.textMuted} dimColor>{template.description}</Text>
|
||||
<Box>
|
||||
<Text color={colors.textMuted} dimColor>{template.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box width="50%" flexDirection="column">
|
||||
<Text color={colors.primary} bold>Action: </Text>
|
||||
<Text color={colors.text}>{action?.name ?? invitation.data.actionIdentifier}</Text>
|
||||
{action?.description && (
|
||||
<Text color={colors.textMuted} dimColor>{action.description}</Text>
|
||||
)}
|
||||
{/* Action info */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={colors.primary} bold>Action:</Text>
|
||||
</Box>
|
||||
<Box width="50%" flexDirection="column">
|
||||
<Text color={colors.primary} bold>Status: </Text>
|
||||
<Box>
|
||||
<Text color={colors.text}>{action?.name ?? invitation.data.actionIdentifier}</Text>
|
||||
</Box>
|
||||
{action?.description && (
|
||||
<Box>
|
||||
<Text color={colors.textMuted} dimColor>{action.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Status */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={colors.primary} bold>Status:</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={stateColor(state)}>{state}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Roles already filled */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={colors.primary} bold>Roles Filled ({filledRoles.size}):</Text>
|
||||
<Box>
|
||||
<Text color={colors.primary} bold>Roles Filled ({filledRoles.size}):</Text>
|
||||
</Box>
|
||||
|
||||
{filledRoles.size === 0 ? (
|
||||
<Text color={colors.textMuted}> None yet</Text>
|
||||
<Box>
|
||||
<Text color={colors.textMuted}> None yet</Text>
|
||||
</Box>
|
||||
) : (
|
||||
Array.from(filledRoles).map(role => {
|
||||
const roleInfoRaw = template?.roles?.[role];
|
||||
const roleInfo = roleInfoRaw && typeof roleInfoRaw === 'object' ? roleInfoRaw : null;
|
||||
return (
|
||||
<Text key={role} color={colors.text}> • {roleInfo?.name ?? role}</Text>
|
||||
<Box key={role}>
|
||||
<Text color={colors.text}> • {roleInfo?.name ?? role}</Text>
|
||||
</Box>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Inputs & Outputs side by side */}
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box width="50%" flexDirection="column">
|
||||
{/* Inputs */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={colors.primary} bold>Inputs ({inputs.length}):</Text>
|
||||
{inputs.length === 0 ? (
|
||||
</Box>
|
||||
|
||||
{inputs.length === 0 ? (
|
||||
<Box>
|
||||
<Text color={colors.textMuted}> None yet</Text>
|
||||
) : (
|
||||
inputs.map((input, idx) => {
|
||||
const inputTemplate = template?.inputs?.[input.inputIdentifier ?? ''];
|
||||
return (
|
||||
<Text key={`input-${idx}`} color={colors.text}>
|
||||
</Box>
|
||||
) : (
|
||||
inputs.map((input, idx) => {
|
||||
const inputTemplate = template?.inputs?.[input.inputIdentifier ?? ''];
|
||||
return (
|
||||
<Box key={`input-${idx}`}>
|
||||
<Text color={colors.text}>
|
||||
{' '}• {inputTemplate?.name ?? input.inputIdentifier ?? `Input ${idx}`}
|
||||
{input.roleIdentifier && ` (${input.roleIdentifier})`}
|
||||
</Text>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Outputs */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={colors.primary} bold>Outputs ({outputs.length}):</Text>
|
||||
</Box>
|
||||
|
||||
<Box width="50%" flexDirection="column">
|
||||
<Text color={colors.primary} bold>Outputs ({outputs.length}):</Text>
|
||||
{outputs.length === 0 ? (
|
||||
{outputs.length === 0 ? (
|
||||
<Box>
|
||||
<Text color={colors.textMuted}> None yet</Text>
|
||||
) : (
|
||||
outputs.map((output, idx) => {
|
||||
const outputTemplate = template?.outputs?.[output.outputIdentifier ?? ''];
|
||||
return (
|
||||
<Text key={`output-${idx}`} color={colors.text}>
|
||||
</Box>
|
||||
) : (
|
||||
outputs.map((output, idx) => {
|
||||
const outputTemplate = template?.outputs?.[output.outputIdentifier ?? ''];
|
||||
return (
|
||||
<Box key={`output-${idx}`}>
|
||||
<Text color={colors.text}>
|
||||
{' '}• {outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`}
|
||||
{output.valueSatoshis !== undefined && ` (${formatSatoshis(output.valueSatoshis)})`}
|
||||
</Text>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Variables */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={colors.primary} bold>Variables ({variables.length}):</Text>
|
||||
<Box>
|
||||
<Text color={colors.primary} bold>Variables ({variables.length}):</Text>
|
||||
</Box>
|
||||
|
||||
{variables.length === 0 ? (
|
||||
<Text color={colors.textMuted}> None set</Text>
|
||||
<Box>
|
||||
<Text color={colors.textMuted}> None set</Text>
|
||||
</Box>
|
||||
) : (
|
||||
variables.map((variable, idx) => {
|
||||
const varTemplate = template?.variables?.[variable.variableIdentifier];
|
||||
@@ -150,9 +192,11 @@ export function PreviewInvitationStep({
|
||||
? variable.value.toString()
|
||||
: String(variable.value);
|
||||
return (
|
||||
<Text key={`var-${idx}`} color={colors.text}>
|
||||
{' '}• {varTemplate?.name ?? variable.variableIdentifier}: {displayValue}
|
||||
</Text>
|
||||
<Box key={`var-${idx}`}>
|
||||
<Text color={colors.text}>
|
||||
{' '}• {varTemplate?.name ?? variable.variableIdentifier}: {displayValue}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { colors, formatSatoshis } from '../../../../theme.js';
|
||||
import { useLayeredInput } from '../../../../hooks/useInputLayer.js';
|
||||
import type { ReviewStepProps, SelectableUTXO } from '../types.js';
|
||||
|
||||
/** Default fee estimate in satoshis. */
|
||||
@@ -55,9 +56,9 @@ export function ReviewStep({
|
||||
}
|
||||
}, [invitation, selectedRole, selectedInputs, onComplete]);
|
||||
|
||||
// Keyboard handling
|
||||
useInput((_input, key) => {
|
||||
if (!isActive || isSubmitting) return;
|
||||
// Keyboard handling — gated by the import-flow layer.
|
||||
useLayeredInput('import-flow', (_input, key) => {
|
||||
if (isSubmitting) return;
|
||||
|
||||
if (key.return) {
|
||||
submit();
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { colors } from '../../../../theme.js';
|
||||
import { useLayeredInput } from '../../../../hooks/useInputLayer.js';
|
||||
import type { RoleSelectStepProps } from '../types.js';
|
||||
|
||||
export function RoleSelectStep({
|
||||
@@ -20,9 +21,7 @@ export function RoleSelectStep({
|
||||
}: RoleSelectStepProps): React.ReactElement {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (!isActive) return;
|
||||
|
||||
useLayeredInput('import-flow', (input, key) => {
|
||||
if (key.upArrow || input === 'k') {
|
||||
setSelectedIndex(prev => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow || input === 'j') {
|
||||
|
||||
Reference in New Issue
Block a user