Blame · Line-by-line history
DiffViewer.tsx
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| c53cf00 | 1 | import React from 'react'; |
| 2 | import { View, Text, ScrollView, StyleSheet } from 'react-native'; | |
| 3 | import { colors } from '../theme/colors'; | |
| 4 | import { fontSizes, fonts } from '../theme/typography'; | |
| 5 | ||
| 6 | interface DiffLine { | |
| 7 | type: 'add' | 'remove' | 'context' | 'header'; | |
| 8 | content: string; | |
| 9 | lineNo?: number; | |
| 10 | } | |
| 11 | ||
| 12 | interface Props { | |
| 13 | diff: string; | |
| 14 | } | |
| 15 | ||
| 16 | function parseDiff(diff: string): DiffLine[] { | |
| 17 | const lines: DiffLine[] = []; | |
| 18 | for (const raw of diff.split('\n')) { | |
| 19 | if (raw.startsWith('@@')) { | |
| 20 | lines.push({ type: 'header', content: raw }); | |
| 21 | } else if (raw.startsWith('+') && !raw.startsWith('+++')) { | |
| 22 | lines.push({ type: 'add', content: raw.slice(1) }); | |
| 23 | } else if (raw.startsWith('-') && !raw.startsWith('---')) { | |
| 24 | lines.push({ type: 'remove', content: raw.slice(1) }); | |
| 25 | } else { | |
| 26 | lines.push({ type: 'context', content: raw.startsWith(' ') ? raw.slice(1) : raw }); | |
| 27 | } | |
| 28 | } | |
| 29 | return lines; | |
| 30 | } | |
| 31 | ||
| 32 | function lineStyle(type: DiffLine['type']) { | |
| 33 | switch (type) { | |
| 34 | case 'add': return { bg: 'rgba(52,211,153,0.10)', prefix: '+', color: colors.green }; | |
| 35 | case 'remove': return { bg: 'rgba(248,113,113,0.10)', prefix: '-', color: colors.red }; | |
| 36 | case 'header': return { bg: 'rgba(140,109,255,0.10)', prefix: '', color: colors.accent }; | |
| 37 | default: return { bg: 'transparent', prefix: ' ', color: colors.textMuted }; | |
| 38 | } | |
| 39 | } | |
| 40 | ||
| 41 | export function DiffViewer({ diff }: Props) { | |
| 42 | const lines = parseDiff(diff); | |
| 43 | ||
| 44 | return ( | |
| 45 | <ScrollView horizontal style={styles.scroll} showsHorizontalScrollIndicator={false}> | |
| 46 | <View style={styles.container}> | |
| 47 | {lines.map((line, i) => { | |
| 48 | const s = lineStyle(line.type); | |
| 49 | return ( | |
| 50 | <View key={i} style={[styles.line, { backgroundColor: s.bg }]}> | |
| 51 | <Text style={[styles.prefix, { color: s.color }]}>{s.prefix}</Text> | |
| 52 | <Text style={[styles.code, { color: s.color }]}> | |
| 53 | {line.content || ' '} | |
| 54 | </Text> | |
| 55 | </View> | |
| 56 | ); | |
| 57 | })} | |
| 58 | </View> | |
| 59 | </ScrollView> | |
| 60 | ); | |
| 61 | } | |
| 62 | ||
| 63 | const styles = StyleSheet.create({ | |
| 64 | scroll: { | |
| 65 | flex: 1, | |
| 66 | backgroundColor: colors.bgSecondary, | |
| 67 | borderRadius: 8, | |
| 68 | }, | |
| 69 | container: { | |
| 70 | padding: 4, | |
| 71 | minWidth: '100%', | |
| 72 | }, | |
| 73 | line: { | |
| 74 | flexDirection: 'row', | |
| 75 | paddingHorizontal: 4, | |
| 76 | paddingVertical: 1, | |
| 77 | borderRadius: 2, | |
| 78 | }, | |
| 79 | prefix: { | |
| 80 | fontSize: fontSizes.xs, | |
| 81 | fontFamily: fonts.mono, | |
| 82 | width: 14, | |
| 83 | textAlign: 'center', | |
| 84 | }, | |
| 85 | code: { | |
| 86 | fontSize: fontSizes.xs, | |
| 87 | fontFamily: fonts.mono, | |
| 88 | lineHeight: 18, | |
| 89 | }, | |
| 90 | }); |