diff --git a/src/codebreaking/crib-analysis.tsx b/src/codebreaking/crib-analysis.tsx index 8f655e6..39ed0ce 100644 --- a/src/codebreaking/crib-analysis.tsx +++ b/src/codebreaking/crib-analysis.tsx @@ -8,6 +8,20 @@ export interface MenuEdge { scramblerTable: string[]; } +export const findConflictIndices = ( + ciphertext: string, + crib: string, + position: number, +): number[] => { + const conflicts: number[] = []; + for (let i = 0; i < crib.length; i++) { + if (crib[i] === ciphertext[position + i]) { + conflicts.push(i); + } + } + return conflicts; +}; + export const findCribPositions = ( ciphertext: string, crib: string, diff --git a/src/codebreaking/index.tsx b/src/codebreaking/index.tsx index c0d7dcb..9af4416 100644 --- a/src/codebreaking/index.tsx +++ b/src/codebreaking/index.tsx @@ -1,6 +1,7 @@ export type { MenuEdge } from './crib-analysis'; export { buildMenuEdges, + findConflictIndices, findCribPositions, propagateMenuConstraints, } from './crib-analysis'; diff --git a/src/components/molecules/CribPositionModal/CribPositionModal.test.tsx b/src/components/molecules/CribPositionModal/CribPositionModal.test.tsx new file mode 100644 index 0000000..3938e65 --- /dev/null +++ b/src/components/molecules/CribPositionModal/CribPositionModal.test.tsx @@ -0,0 +1,128 @@ +import React from 'react'; + +import { + CRIB_POSITION_CONFLICT_HINT, + CRIB_POSITION_MODAL_TITLE, +} from '../../../constants/labels'; +import { + CRIB_POSITION_CLEAR_BUTTON, + CRIB_POSITION_CONFIRM_BUTTON, + CRIB_POSITION_LEFT_ARROW, + CRIB_POSITION_MODAL, + CRIB_POSITION_RIGHT_ARROW, +} from '../../../constants/selectors'; +import { fireEvent, render, screen } from '../../../utils/test-utils'; +import { CribPositionModal } from './CribPositionModal'; + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + setOptions: jest.fn(), + }), + }; +}); + +const defaultProps = { + visible: true, + onDismiss: jest.fn(), + ciphertext: 'ABCDEFGH', + crib: 'XYZ', + currentPosition: undefined as number | undefined, + onConfirm: jest.fn(), + onClear: jest.fn(), +}; + +const renderModal = (overrides = {}) => + render(); + +describe('CribPositionModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders modal with title', async () => { + await renderModal(); + expect(screen.getByTestId(CRIB_POSITION_MODAL)).toBeTruthy(); + expect(screen.getByText(CRIB_POSITION_MODAL_TITLE)).toBeTruthy(); + }); + + it('shows position 1 of N by default', async () => { + await renderModal(); + expect(screen.getByText(/Position 1 of 6/)).toBeTruthy(); + }); + + it('starts at currentPosition when provided', async () => { + await renderModal({ currentPosition: 3 }); + expect(screen.getByText(/Position 4 of 6/)).toBeTruthy(); + }); + + it('navigates right with arrow button', async () => { + await renderModal(); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_RIGHT_ARROW)); + expect(screen.getByText(/Position 2 of 6/)).toBeTruthy(); + }); + + it('navigates left with arrow button', async () => { + await renderModal({ currentPosition: 2 }); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_LEFT_ARROW)); + expect(screen.getByText(/Position 2 of 6/)).toBeTruthy(); + }); + + it('does not navigate left past position 0', async () => { + await renderModal(); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_LEFT_ARROW)); + expect(screen.getByText(/Position 1 of 6/)).toBeTruthy(); + }); + + it('does not navigate right past max position', async () => { + await renderModal({ currentPosition: 5 }); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_RIGHT_ARROW)); + expect(screen.getByText(/Position 6 of 6/)).toBeTruthy(); + }); + + it('shows conflict hint when crib letter matches ciphertext letter', async () => { + await renderModal({ ciphertext: 'AXCDEFGH', crib: 'AYZ' }); + expect(screen.getByText(CRIB_POSITION_CONFLICT_HINT)).toBeTruthy(); + }); + + it('disables confirm button when position has conflicts', async () => { + const onConfirm = jest.fn(); + await renderModal({ ciphertext: 'AXCDEFGH', crib: 'AYZ', onConfirm }); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_CONFIRM_BUTTON)); + expect(onConfirm).not.toHaveBeenCalled(); + }); + + it('does not show conflict hint when no conflicts', async () => { + await renderModal({ ciphertext: 'ABCDEFGH', crib: 'XYZ' }); + expect(screen.queryByText(CRIB_POSITION_CONFLICT_HINT)).toBeNull(); + }); + + it('calls onConfirm with 0-indexed position and dismisses', async () => { + const onConfirm = jest.fn(); + const onDismiss = jest.fn(); + await renderModal({ onConfirm, onDismiss, currentPosition: 2 }); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_CONFIRM_BUTTON)); + expect(onConfirm).toHaveBeenCalledWith(2); + expect(onDismiss).toHaveBeenCalled(); + }); + + it('calls onClear and dismisses', async () => { + const onClear = jest.fn(); + const onDismiss = jest.fn(); + await renderModal({ onClear, onDismiss }); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_CLEAR_BUTTON)); + expect(onClear).toHaveBeenCalled(); + expect(onDismiss).toHaveBeenCalled(); + }); + + it('confirms navigated position after arrow presses', async () => { + const onConfirm = jest.fn(); + await renderModal({ onConfirm }); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_RIGHT_ARROW)); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_RIGHT_ARROW)); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_CONFIRM_BUTTON)); + expect(onConfirm).toHaveBeenCalledWith(2); + }); +}); diff --git a/src/components/molecules/CribPositionModal/CribPositionModal.tsx b/src/components/molecules/CribPositionModal/CribPositionModal.tsx new file mode 100644 index 0000000..1249bf4 --- /dev/null +++ b/src/components/molecules/CribPositionModal/CribPositionModal.tsx @@ -0,0 +1,249 @@ +import type { FunctionComponent } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import type { ScrollView as ScrollViewType } from 'react-native'; +import { ScrollView, Text, View } from 'react-native'; +import { Button, IconButton, Modal } from 'react-native-paper'; + +import { findConflictIndices } from '../../../codebreaking'; +import { + CRIB_POSITION_CLEAR, + CRIB_POSITION_CONFIRM, + CRIB_POSITION_CONFLICT_HINT, + CRIB_POSITION_MODAL_TITLE, + POSITION_LABEL, +} from '../../../constants/labels'; +import { + CRIB_POSITION_CLEAR_BUTTON, + CRIB_POSITION_CONFIRM_BUTTON, + CRIB_POSITION_INDICATOR, + CRIB_POSITION_LEFT_ARROW, + CRIB_POSITION_MODAL, + CRIB_POSITION_RIGHT_ARROW, +} from '../../../constants/selectors'; +import { useThemeColors } from '../../../theme/useThemeColors'; +import { CELL_SIZE, makeStyles } from './styles'; + +interface CribPositionModalProps { + visible: boolean; + onDismiss: () => void; + ciphertext: string; + crib: string; + currentPosition: number | undefined; + onConfirm: (position: number) => void; + onClear: () => void; +} + +export const CribPositionModal: FunctionComponent = ({ + visible, + onDismiss, + ciphertext, + crib, + currentPosition, + onConfirm, + onClear, +}) => { + const maxPosition = Math.max(0, ciphertext.length - crib.length); + const [position, setPosition] = useState(currentPosition ?? 0); + const scrollRef = useRef(null); + const colors = useThemeColors(); + const styles = useMemo(() => makeStyles(colors), [colors]); + + useEffect(() => { + if (visible) { + setPosition(currentPosition ?? 0); + } + }, [visible, currentPosition]); + + const scrollToPosition = useCallback( + (pos: number) => { + scrollRef.current?.scrollTo({ + x: Math.max(0, (pos + crib.length / 2) * CELL_SIZE - 150), + animated: true, + }); + }, + [crib.length], + ); + + useEffect(() => { + if (visible) { + const timer = setTimeout(() => scrollToPosition(position), 100); + return () => clearTimeout(timer); + } + }, [visible, position, scrollToPosition]); + + const conflictIndices = useMemo( + () => findConflictIndices(ciphertext, crib, position), + [ciphertext, crib, position], + ); + + const hasConflicts = conflictIndices.length > 0; + const conflictSet = useMemo( + () => new Set(conflictIndices), + [conflictIndices], + ); + + const handleLeft = useCallback(() => { + setPosition((p) => Math.max(0, p - 1)); + }, []); + + const handleRight = useCallback(() => { + setPosition((p) => Math.min(maxPosition, p + 1)); + }, [maxPosition]); + + const handleConfirm = useCallback(() => { + onConfirm(position); + onDismiss(); + }, [position, onConfirm, onDismiss]); + + const handleClear = useCallback(() => { + onClear(); + onDismiss(); + }, [onClear, onDismiss]); + + const ciphertextLetters = useMemo( + () => + ciphertext.split('').map((letter, pos) => ({ + key: `ct-${pos}`, + letter, + pos, + })), + [ciphertext], + ); + + const ciphertextCells = useMemo( + () => + ciphertextLetters.map(({ key, letter, pos }) => { + const cribIndex = pos - position; + const isConflict = + cribIndex >= 0 && + cribIndex < crib.length && + conflictSet.has(cribIndex); + return ( + + + {letter} + + + ); + }), + [ciphertextLetters, position, crib.length, conflictSet, styles], + ); + + const cribLetters = useMemo( + () => + crib.split('').map((letter, pos) => ({ + key: `cr-${pos}`, + letter, + pos, + })), + [crib], + ); + + const cribCells = useMemo(() => { + const cells: React.ReactNode[] = []; + for (let s = 0; s < position; s++) { + cells.push(); + } + cribLetters.forEach(({ key, letter, pos }) => { + const isConflict = conflictSet.has(pos); + cells.push( + + + {letter} + + , + ); + }); + return cells; + }, [cribLetters, position, conflictSet, styles]); + + return ( + + {CRIB_POSITION_MODAL_TITLE} + + + + {ciphertextCells} + {cribCells} + + + + + + + {POSITION_LABEL} {position + 1} of {maxPosition + 1} + + + + + {hasConflicts && ( + {CRIB_POSITION_CONFLICT_HINT} + )} + + + + + + + ); +}; diff --git a/src/components/molecules/CribPositionModal/index.tsx b/src/components/molecules/CribPositionModal/index.tsx new file mode 100644 index 0000000..82010f2 --- /dev/null +++ b/src/components/molecules/CribPositionModal/index.tsx @@ -0,0 +1 @@ +export * from './CribPositionModal'; diff --git a/src/components/molecules/CribPositionModal/styles.ts b/src/components/molecules/CribPositionModal/styles.ts new file mode 100644 index 0000000..f7c92c5 --- /dev/null +++ b/src/components/molecules/CribPositionModal/styles.ts @@ -0,0 +1,107 @@ +import { StyleSheet } from 'react-native'; + +import type { ColorPalette } from '../../../theme/colors'; + +const CELL_SIZE = 32; + +export const makeStyles = (colors: ColorPalette) => + StyleSheet.create({ + container: { + backgroundColor: colors.surface, + padding: 24, + marginHorizontal: 16, + borderRadius: 12, + }, + title: { + color: colors.textPrimary, + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + marginBottom: 20, + }, + scrollContainer: { + marginBottom: 20, + }, + row: { + flexDirection: 'row', + }, + cribRow: { + flexDirection: 'row', + marginTop: 2, + }, + cell: { + width: CELL_SIZE, + height: CELL_SIZE, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.surfaceAlt, + borderWidth: 1, + borderColor: colors.border, + borderRadius: 4, + marginHorizontal: 1, + }, + cellText: { + color: colors.textPrimary, + fontSize: 14, + fontWeight: '700', + fontFamily: 'monospace', + }, + cribCell: { + width: CELL_SIZE, + height: CELL_SIZE, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.accent, + borderWidth: 1, + borderColor: colors.accent, + borderRadius: 4, + marginHorizontal: 1, + }, + cribCellText: { + color: colors.background, + fontSize: 14, + fontWeight: '700', + fontFamily: 'monospace', + }, + emptyCell: { + width: CELL_SIZE, + height: CELL_SIZE, + marginHorizontal: 1, + }, + conflictCell: { + backgroundColor: colors.destructive, + borderColor: colors.destructive, + }, + conflictText: { + color: '#FFFFFF', + }, + navRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 12, + }, + positionText: { + color: colors.textPrimary, + fontSize: 14, + fontWeight: '500', + minWidth: 120, + textAlign: 'center', + }, + hintText: { + color: colors.destructive, + fontSize: 12, + textAlign: 'center', + marginBottom: 12, + }, + buttonRow: { + flexDirection: 'row', + justifyContent: 'space-between', + gap: 12, + }, + button: { + flex: 1, + }, + }); + +export { CELL_SIZE }; diff --git a/src/components/pages/AppSettings/styles.ts b/src/components/pages/AppSettings/styles.ts index a457c56..a103850 100644 --- a/src/components/pages/AppSettings/styles.ts +++ b/src/components/pages/AppSettings/styles.ts @@ -4,9 +4,7 @@ import type { ColorPalette } from '../../../theme/colors'; export const makeStyles = (colors: ColorPalette) => StyleSheet.create({ - scrollContent: { - paddingVertical: 20, - }, + scrollContent: {}, label: { color: colors.textPrimary, fontSize: 15, diff --git a/src/components/pages/about/styles.ts b/src/components/pages/about/styles.ts index 6610e1a..db05922 100644 --- a/src/components/pages/about/styles.ts +++ b/src/components/pages/about/styles.ts @@ -4,9 +4,7 @@ import type { ColorPalette } from '../../../theme/colors'; export const makeStyles = (colors: ColorPalette) => StyleSheet.create({ - scrollContent: { - paddingVertical: 20, - }, + scrollContent: {}, title: { color: colors.accent, fontSize: 24, diff --git a/src/components/pages/breakCipher/BreakCipher.test.tsx b/src/components/pages/breakCipher/BreakCipher.test.tsx index a1d0749..6c23bae 100644 --- a/src/components/pages/breakCipher/BreakCipher.test.tsx +++ b/src/components/pages/breakCipher/BreakCipher.test.tsx @@ -8,7 +8,11 @@ import { CIPHERTEXT_INPUT, COPY_MESSAGE_BUTTON, CRIB_INPUT, + CRIB_POSITION_BUTTON, CRIB_POSITION_CARD, + CRIB_POSITION_CONFIRM_BUTTON, + CRIB_POSITION_MODAL, + CRIB_POSITION_RIGHT_ARROW, DECRYPTED_TEXT_DISPLAY, PROGRESS_BAR, RESULTS_CONTAINER, @@ -264,4 +268,46 @@ describe('BreakCipher', () => { await fireEvent.changeText(screen.getByTestId(CRIB_INPUT), 'cr1b!'); expect(screen.getByTestId(CRIB_INPUT).props['value']).toBe('CRB'); }); + + it('crib position button is disabled when ciphertext or crib is empty', async () => { + await render(); + const button = screen.getByTestId(CRIB_POSITION_BUTTON); + expect( + (button.props as { accessibilityState?: { disabled?: boolean } }) + .accessibilityState?.disabled, + ).toBe(true); + }); + + it('crib position button is enabled when ciphertext and crib are filled', async () => { + await render(); + await fireEvent.changeText(screen.getByTestId(CIPHERTEXT_INPUT), 'ABCDEF'); + await fireEvent.changeText(screen.getByTestId(CRIB_INPUT), 'XY'); + const button = screen.getByTestId(CRIB_POSITION_BUTTON); + expect( + (button.props as { accessibilityState?: { disabled?: boolean } }) + .accessibilityState?.disabled, + ).toBe(false); + }); + + it('opens crib position modal on button press', async () => { + await render(); + await fireEvent.changeText(screen.getByTestId(CIPHERTEXT_INPUT), 'ABCDEF'); + await fireEvent.changeText(screen.getByTestId(CRIB_INPUT), 'XY'); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_BUTTON)); + expect(screen.getByTestId(CRIB_POSITION_MODAL)).toBeTruthy(); + }); + + it('confirms crib position from modal and updates button label', async () => { + await render(); + await fireEvent.changeText(screen.getByTestId(CIPHERTEXT_INPUT), 'ABCDEF'); + await fireEvent.changeText(screen.getByTestId(CRIB_INPUT), 'XY'); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_BUTTON)); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_RIGHT_ARROW)); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_RIGHT_ARROW)); + await fireEvent.press(screen.getByTestId(CRIB_POSITION_CONFIRM_BUTTON)); + + await waitFor(() => { + expect(screen.getByText(/Position: 3/)).toBeTruthy(); + }); + }); }); diff --git a/src/components/pages/breakCipher/BreakCipher.tsx b/src/components/pages/breakCipher/BreakCipher.tsx index 18e51d3..fe1be1b 100644 --- a/src/components/pages/breakCipher/BreakCipher.tsx +++ b/src/components/pages/breakCipher/BreakCipher.tsx @@ -11,16 +11,17 @@ import { CIPHERTEXT_LABEL, COMMON_CRIBS_HINT, CRIB_LABEL, - CRIB_POSITION_LABEL, + CRIB_POSITION_BUTTON_LABEL, INFO_CRIB_ANALYSIS_CONTENT, INFO_CRIB_ANALYSIS_TITLE, + POSITION_LABEL, SAVE_RESULTS_LABEL, } from '../../../constants/labels'; import { CANCEL_SEARCH_BUTTON, CIPHERTEXT_INPUT, CRIB_INPUT, - CRIB_POSITION_INPUT, + CRIB_POSITION_BUTTON, INFO_BUTTON, RESULTS_CONTAINER, SAVE_RESULTS_BUTTON, @@ -34,6 +35,7 @@ import type { AppDispatch, RootState } from '../../../store/store'; import { useThemeColors } from '../../../theme/useThemeColors'; import type { SavedAnalysis } from '../../../types/interfaces'; import { addSavedAnalysis } from '../../../utils/storage'; +import { CribPositionModal } from '../../molecules/CribPositionModal'; import { InfoSidebar } from '../../molecules/InfoSidebar'; import { RunButton } from '../../molecules/RunButton'; import { CribSearchResults } from '../../organisms/CribSearchResults'; @@ -67,7 +69,10 @@ export const BreakCipher: FunctionComponent = () => { const [infoVisible, setInfoVisible] = useState(false); const [ciphertext, setCiphertext] = useState(''); const [crib, setCrib] = useState(''); - const [cribPosition, setCribPosition] = useState(''); + const [cribPosition, setCribPosition] = useState( + undefined, + ); + const [cribModalVisible, setCribModalVisible] = useState(false); const [expandedPosition, setExpandedPosition] = useState(null); const colors = useThemeColors(); @@ -94,24 +99,12 @@ export const BreakCipher: FunctionComponent = () => { dispatch(searchCancelled()); }, [dispatch]); - const parsedCribPosition = useMemo(() => { - const trimmed = cribPosition.trim(); - if (trimmed === '') return undefined; - const parsed = parseInt(trimmed, 10); - return Number.isNaN(parsed) || parsed < 0 ? undefined : parsed; - }, [cribPosition]); - const handleRun = useCallback(() => { const sanitizedCiphertext = sanitizeInput(ciphertext); const sanitizedCrib = sanitizeInput(crib); if (!sanitizedCiphertext || !sanitizedCrib) return; - runCribAnalysis( - sanitizedCiphertext, - sanitizedCrib, - dispatch, - parsedCribPosition, - ); - }, [ciphertext, crib, dispatch, parsedCribPosition]); + runCribAnalysis(sanitizedCiphertext, sanitizedCrib, dispatch, cribPosition); + }, [ciphertext, crib, dispatch, cribPosition]); const toggleExpandedPosition = useCallback( (pos: number) => { @@ -145,10 +138,7 @@ export const BreakCipher: FunctionComponent = () => { const runAnalysisButtonDisabled = !isCribReady || isSearching; return ( - + { textColor={colors.textPrimary} outlineColor={colors.border} activeOutlineColor={colors.accent} - theme={{ colors: { onSurfaceVariant: colors.textSecondary } }} + theme={{ + colors: { + onSurfaceVariant: colors.textSecondary, + background: cribModalVisible ? 'transparent' : colors.surface, + }, + }} /> { textColor={colors.textPrimary} outlineColor={colors.border} activeOutlineColor={colors.accent} - theme={{ colors: { onSurfaceVariant: colors.textSecondary } }} + theme={{ + colors: { + onSurfaceVariant: colors.textSecondary, + background: cribModalVisible ? 'transparent' : colors.surface, + }, + }} /> - setCribModalVisible(true)} + disabled={!isCribReady} + style={styles.positionButton} textColor={colors.textPrimary} - outlineColor={colors.border} - activeOutlineColor={colors.accent} - theme={{ colors: { onSurfaceVariant: colors.textSecondary } }} - /> + icon={ + cribPosition !== undefined + ? 'check-circle-outline' + : 'cursor-default-click-outline' + } + theme={{ + colors: { + surfaceDisabled: colors.disabledSurface, + onSurfaceDisabled: colors.disabledText, + }, + }} + > + {cribPosition !== undefined + ? `${POSITION_LABEL}: ${cribPosition + 1}` + : CRIB_POSITION_BUTTON_LABEL} + {COMMON_CRIBS_HINT} @@ -246,6 +256,16 @@ export const BreakCipher: FunctionComponent = () => { title={INFO_CRIB_ANALYSIS_TITLE} content={INFO_CRIB_ANALYSIS_CONTENT} /> + + setCribModalVisible(false)} + ciphertext={sanitizeInput(ciphertext)} + crib={sanitizeInput(crib)} + currentPosition={cribPosition} + onConfirm={setCribPosition} + onClear={() => setCribPosition(undefined)} + /> ); }; diff --git a/src/components/pages/breakCipher/styles.ts b/src/components/pages/breakCipher/styles.ts index 5587a81..22f9c73 100644 --- a/src/components/pages/breakCipher/styles.ts +++ b/src/components/pages/breakCipher/styles.ts @@ -4,13 +4,11 @@ import type { ColorPalette } from '../../../theme/colors'; export const makeStyles = (colors: ColorPalette) => StyleSheet.create({ - scrollPadding: { - paddingVertical: 16, + contentContainer: { + height: '100%', }, - contentContainer: {}, input: { marginBottom: 12, - backgroundColor: colors.surface, }, cancelButton: { marginTop: 4, @@ -19,6 +17,10 @@ export const makeStyles = (colors: ColorPalette) => saveButton: { marginVertical: 12, }, + positionButton: { + marginBottom: 12, + borderColor: colors.border, + }, hintText: { color: colors.textSecondary, fontSize: 12, diff --git a/src/components/pages/savedAnalyses/styles.ts b/src/components/pages/savedAnalyses/styles.ts index b8bd88f..9500924 100644 --- a/src/components/pages/savedAnalyses/styles.ts +++ b/src/components/pages/savedAnalyses/styles.ts @@ -4,9 +4,7 @@ import type { ColorPalette } from '../../../theme/colors'; export const makeStyles = (colors: ColorPalette) => StyleSheet.create({ - scrollContent: { - paddingVertical: 16, - }, + scrollContent: {}, title: { color: colors.accent, fontSize: 20, diff --git a/src/components/pages/savedMessages/styles.ts b/src/components/pages/savedMessages/styles.ts index 378c86d..04ea224 100644 --- a/src/components/pages/savedMessages/styles.ts +++ b/src/components/pages/savedMessages/styles.ts @@ -4,9 +4,7 @@ import type { ColorPalette } from '../../../theme/colors'; export const makeStyles = (colors: ColorPalette) => StyleSheet.create({ - scrollContent: { - paddingVertical: 16, - }, + scrollContent: {}, title: { color: colors.accent, fontSize: 20, diff --git a/src/components/templates/Page/Page.tsx b/src/components/templates/Page/Page.tsx index e19cb44..1dcfb6e 100644 --- a/src/components/templates/Page/Page.tsx +++ b/src/components/templates/Page/Page.tsx @@ -10,6 +10,7 @@ const PAGE_HORIZONTAL_PADDING = 16; const styles = StyleSheet.create({ scrollFlex: { flex: 1 }, + contentPadding: { padding: PAGE_HORIZONTAL_PADDING }, }); interface PageProps { @@ -31,7 +32,6 @@ export const Page: FunctionComponent = ({ () => ({ flex: 1 as const, backgroundColor: colors.background, - paddingHorizontal: PAGE_HORIZONTAL_PADDING, }), [colors.background], ); @@ -41,7 +41,7 @@ export const Page: FunctionComponent = ({ {children} @@ -50,7 +50,10 @@ export const Page: FunctionComponent = ({ } return ( - + {children} ); diff --git a/src/constants/labels.tsx b/src/constants/labels.tsx index 27fae4c..332ad94 100644 --- a/src/constants/labels.tsx +++ b/src/constants/labels.tsx @@ -28,6 +28,12 @@ export const VALID_POSITIONS_LABEL = 'Valid crib positions'; export const POSITION_LABEL = 'Position'; export const CRIB_PREFIX_LABEL = 'Crib'; export const CRIB_POSITION_LABEL = 'Crib position (optional)'; +export const CRIB_POSITION_BUTTON_LABEL = 'Set crib position (optional)'; +export const CRIB_POSITION_MODAL_TITLE = 'Slide crib to position'; +export const CRIB_POSITION_CONFIRM = 'Confirm'; +export const CRIB_POSITION_CLEAR = 'Clear'; +export const CRIB_POSITION_CONFLICT_HINT = + 'Invalid — a letter cannot encrypt to itself'; export const COMMON_CRIBS_HINT = 'Common cribs: WETTER, KEINE, OBERKOMMANDO'; export const TAP_TO_EXPAND = 'Tap a position to see alignment'; export const DECRYPTED_TEXT_LABEL = 'Decrypted'; diff --git a/src/constants/selectors.tsx b/src/constants/selectors.tsx index 2fc74b0..b721213 100644 --- a/src/constants/selectors.tsx +++ b/src/constants/selectors.tsx @@ -17,6 +17,13 @@ export const CIPHERTEXT_INPUT = 'ciphertextInput'; export const PLAINTEXT_INPUT = 'plaintextInput'; export const CRIB_INPUT = 'cribInput'; export const CRIB_POSITION_INPUT = 'cribPositionInput'; +export const CRIB_POSITION_BUTTON = 'cribPositionButton'; +export const CRIB_POSITION_MODAL = 'cribPositionModal'; +export const CRIB_POSITION_LEFT_ARROW = 'cribPositionLeftArrow'; +export const CRIB_POSITION_RIGHT_ARROW = 'cribPositionRightArrow'; +export const CRIB_POSITION_CONFIRM_BUTTON = 'cribPositionConfirmButton'; +export const CRIB_POSITION_CLEAR_BUTTON = 'cribPositionClearButton'; +export const CRIB_POSITION_INDICATOR = 'cribPositionIndicator'; export const RUN_ANALYSIS_BUTTON = 'runAnalysisButton'; export const BRUTE_FORCE_TAB_BUTTON = 'bruteForceTab'; export const CRIB_ANALYSIS_TAB_BUTTON = 'cribAnalysisTab'; diff --git a/src/theme/colors.tsx b/src/theme/colors.tsx index 299b76c..f02c4b5 100644 --- a/src/theme/colors.tsx +++ b/src/theme/colors.tsx @@ -9,6 +9,9 @@ export interface ColorPalette { destructive: string; disabledSurface: string; disabledText: string; + surfaceDisabled: string; + onSurfaceDisabled: string; + backdrop: string; } export const darkColors: ColorPalette = { @@ -22,6 +25,9 @@ export const darkColors: ColorPalette = { destructive: '#9c2a2a', disabledSurface: '#3a3a3a', disabledText: '#666666', + surfaceDisabled: 'rgba(245, 240, 232, 0.12)', + onSurfaceDisabled: 'rgba(245, 240, 232, 0.38)', + backdrop: 'rgba(0, 0, 0, 0.6)', }; export const lightColors: ColorPalette = { @@ -35,6 +41,9 @@ export const lightColors: ColorPalette = { destructive: '#9c2a2a', disabledSurface: '#C5BAA8', disabledText: '#7A6A5A', + surfaceDisabled: 'rgba(245, 240, 232, 0.12)', + onSurfaceDisabled: 'rgba(245, 240, 232, 0.38)', + backdrop: 'rgba(0, 0, 0, 0.6)', }; export const nlpColors = {