diff --git a/src/__tests__/TrackRow.test.tsx b/src/__tests__/TrackRow.test.tsx new file mode 100644 index 0000000..cd9b67e --- /dev/null +++ b/src/__tests__/TrackRow.test.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import TestRenderer from 'react-test-renderer'; + +import { TrackRow } from '../components/TrackRow'; + +const baseProps = { + trackNumber: 3, + day: '15/06', + hour: '19:30', + title: 'Sparring', + meta: '30 min · 600 amostras', + metrics: [ + { label: 'MÉD 152' }, + { label: 'PICO 184', tone: 'hot' as const }, + ], + isOpen: false, + onPress: jest.fn(), + accessibilityLabel: 'Janela 3', +}; + +describe('TrackRow', () => { + beforeEach(() => { + baseProps.onPress.mockClear(); + }); + + it('renders track number zero-padded', () => { + let tree: any; + TestRenderer.act(() => { + tree = TestRenderer.create(React.createElement(TrackRow, { ...baseProps, testID: 'row' })); + }); + const json = JSON.stringify(tree.toJSON()); + expect(json).toContain('T03'); + }); + + it('renders day, hour, title, and meta', () => { + let tree: any; + TestRenderer.act(() => { + tree = TestRenderer.create(React.createElement(TrackRow, { ...baseProps, testID: 'row' })); + }); + const json = JSON.stringify(tree.toJSON()); + expect(json).toContain('15/06'); + expect(json).toContain('19:30'); + expect(json).toContain('Sparring'); + expect(json).toContain('30 min · 600 amostras'); + }); + + it('renders metrics labels', () => { + let tree: any; + TestRenderer.act(() => { + tree = TestRenderer.create(React.createElement(TrackRow, { ...baseProps, testID: 'row' })); + }); + const json = JSON.stringify(tree.toJSON()); + expect(json).toContain('MÉD 152'); + expect(json).toContain('PICO 184'); + }); +}); diff --git a/src/components/TrackRow.tsx b/src/components/TrackRow.tsx new file mode 100644 index 0000000..fab956d --- /dev/null +++ b/src/components/TrackRow.tsx @@ -0,0 +1,165 @@ +import React, { type ReactNode } from 'react'; +import { Pressable, StyleSheet, Text, View, type ViewStyle } from 'react-native'; +import { ChevronDown, ChevronUp } from 'lucide-react-native'; +import { colors, spacing, typography } from '../theme'; + +type MetricTone = 'normal' | 'hot'; + +type Metric = { + readonly label: string; + readonly tone?: MetricTone; +}; + +type Props = { + readonly trackNumber: number; + readonly day: string; + readonly hour: string; + readonly title: string; + readonly meta: string; + readonly metrics: ReadonlyArray; + readonly isOpen: boolean; + readonly onPress: () => void; + readonly accessibilityLabel: string; + readonly children?: ReactNode; + readonly testID?: string; +}; + +export function TrackRow({ + trackNumber, + day, + hour, + title, + meta, + metrics, + isOpen, + onPress, + accessibilityLabel, + children, + testID, +}: Props) { + const trackNumberLabel = `T${String(trackNumber).padStart(2, '0')}`; + const sideRailStyle: ViewStyle = { + backgroundColor: isOpen ? colors.action : colors.line, + }; + return ( + + [styles.row, pressed ? styles.pressed : null]} + testID={testID} + > + + + {trackNumberLabel} + + + {day} + {hour} + + + {title} + {meta} + + {metrics.map((metric, index) => ( + + {metric.label} + + ))} + + + + {isOpen ? ( + + ) : ( + + )} + + + {children} + + ); +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + gap: spacing.sm, + borderTopWidth: 1, + borderTopColor: colors.line, + }, + pressed: { + opacity: 0.72, + }, + sideRail: { + width: 2, + height: 40, + }, + trackNumberWrap: { + width: 28, + alignItems: 'flex-start', + }, + trackNumber: { + ...typography.label, + color: colors.textMuted, + }, + dateBlock: { + width: 56, + }, + day: { + color: colors.warning, + fontSize: 18, + lineHeight: 21, + fontWeight: '900', + letterSpacing: -0.6, + }, + hour: { + ...typography.label, + color: colors.textDim, + marginTop: 2, + }, + body: { + flex: 1, + }, + title: { + color: colors.text, + fontSize: 18, + lineHeight: 22, + fontWeight: '900', + letterSpacing: -0.6, + }, + meta: { + ...typography.body, + color: colors.textMuted, + fontSize: 13, + lineHeight: 17, + marginTop: 2, + }, + metricLine: { + flexDirection: 'row', + gap: spacing.sm, + marginTop: spacing.xs, + }, + metricText: { + ...typography.label, + color: colors.textMuted, + }, + metricTextHot: { + color: colors.action, + }, + chevron: { + width: 18, + alignItems: 'center', + justifyContent: 'center', + }, +});