• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

agentic-dev-library / thumbcode / 21118660341

18 Jan 2026 09:10PM UTC coverage: 28.729% (-14.5%) from 43.195%
21118660341

Pull #50

github

web-flow
Merge da0b08c49 into c7c91f5ce
Pull Request #50: feat(components): expand component library to production-ready set

181 of 1095 branches covered (16.53%)

Branch coverage included in aggregate %.

0 of 309 new or added lines in 21 files covered. (0.0%)

495 of 1258 relevant lines covered (39.35%)

1.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/components/code/DiffViewer.tsx
1
/**
2
 * DiffViewer Component
3
 *
4
 * Displays code diffs with line-by-line highlighting.
5
 * Supports unified and split view modes.
6
 */
7

8
import { useState } from 'react';
9
import { Pressable, ScrollView, Text, View } from 'react-native';
10

11
interface DiffLine {
12
  type: 'add' | 'remove' | 'context';
13
  content: string;
14
  oldLineNumber?: number;
15
  newLineNumber?: number;
16
}
17

18
interface DiffViewerProps {
19
  /** Old file content */
20
  oldContent?: string;
21
  /** New file content */
22
  newContent?: string;
23
  /** Pre-parsed diff lines */
24
  diff?: DiffLine[];
25
  /** File path */
26
  filename?: string;
27
  /** View mode */
28
  viewMode?: 'unified' | 'split';
29
  /** Show line numbers */
30
  showLineNumbers?: boolean;
31
}
32

33
function parseDiff(oldContent: string, newContent: string): DiffLine[] {
34
  // Simple line-by-line diff (in production, use a proper diff library)
NEW
35
  const oldLines = oldContent.split('\n');
×
NEW
36
  const newLines = newContent.split('\n');
×
NEW
37
  const result: DiffLine[] = [];
×
38

NEW
39
  let oldIndex = 0;
×
NEW
40
  let newIndex = 0;
×
41

NEW
42
  while (oldIndex < oldLines.length || newIndex < newLines.length) {
×
NEW
43
    if (oldIndex >= oldLines.length) {
×
NEW
44
      result.push({
×
45
        type: 'add',
46
        content: newLines[newIndex],
47
        newLineNumber: newIndex + 1,
48
      });
NEW
49
      newIndex++;
×
NEW
50
    } else if (newIndex >= newLines.length) {
×
NEW
51
      result.push({
×
52
        type: 'remove',
53
        content: oldLines[oldIndex],
54
        oldLineNumber: oldIndex + 1,
55
      });
NEW
56
      oldIndex++;
×
NEW
57
    } else if (oldLines[oldIndex] === newLines[newIndex]) {
×
NEW
58
      result.push({
×
59
        type: 'context',
60
        content: oldLines[oldIndex],
61
        oldLineNumber: oldIndex + 1,
62
        newLineNumber: newIndex + 1,
63
      });
NEW
64
      oldIndex++;
×
NEW
65
      newIndex++;
×
66
    } else {
67
      // Lines differ - emit both remove and add
NEW
68
      result.push({
×
69
        type: 'remove',
70
        content: oldLines[oldIndex],
71
        oldLineNumber: oldIndex + 1,
72
      });
NEW
73
      result.push({
×
74
        type: 'add',
75
        content: newLines[newIndex],
76
        newLineNumber: newIndex + 1,
77
      });
NEW
78
      oldIndex++;
×
NEW
79
      newIndex++;
×
80
    }
81
  }
82

NEW
83
  return result;
×
84
}
85

86
export function DiffViewer({
87
  oldContent,
88
  newContent,
89
  diff,
90
  filename,
91
  viewMode: _viewMode = 'unified',
×
92
  showLineNumbers = true,
×
93
}: DiffViewerProps) {
94
  // Note: viewMode 'split' is not yet implemented, only 'unified' is used
NEW
95
  void _viewMode;
×
NEW
96
  const [collapsed, setCollapsed] = useState(false);
×
97

NEW
98
  const lines = diff || (oldContent && newContent ? parseDiff(oldContent, newContent) : []);
×
NEW
99
  const additions = lines.filter((l) => l.type === 'add').length;
×
NEW
100
  const deletions = lines.filter((l) => l.type === 'remove').length;
×
101

NEW
102
  const getLineStyle = (type: DiffLine['type']) => {
×
NEW
103
    switch (type) {
×
104
      case 'add':
NEW
105
        return 'bg-teal-600/20';
×
106
      case 'remove':
NEW
107
        return 'bg-coral-500/20';
×
108
      default:
NEW
109
        return 'bg-transparent';
×
110
    }
111
  };
112

NEW
113
  const getLinePrefix = (type: DiffLine['type']) => {
×
NEW
114
    switch (type) {
×
115
      case 'add':
NEW
116
        return '+';
×
117
      case 'remove':
NEW
118
        return '-';
×
119
      default:
NEW
120
        return ' ';
×
121
    }
122
  };
123

NEW
124
  const getPrefixColor = (type: DiffLine['type']) => {
×
NEW
125
    switch (type) {
×
126
      case 'add':
NEW
127
        return 'text-teal-400';
×
128
      case 'remove':
NEW
129
        return 'text-coral-400';
×
130
      default:
NEW
131
        return 'text-neutral-500';
×
132
    }
133
  };
134

NEW
135
  return (
×
136
    <View
137
      className="bg-charcoal overflow-hidden"
138
      style={{
139
        borderTopLeftRadius: 12,
140
        borderTopRightRadius: 10,
141
        borderBottomRightRadius: 14,
142
        borderBottomLeftRadius: 8,
143
      }}
144
    >
145
      {/* Header */}
146
      <Pressable
NEW
147
        onPress={() => setCollapsed(!collapsed)}
×
148
        className="flex-row items-center justify-between px-3 py-2 bg-neutral-800 border-b border-neutral-700"
149
      >
150
        <View className="flex-row items-center flex-1">
151
          <Text className="text-neutral-400 mr-2">{collapsed ? '▶' : '▼'}</Text>
×
152
          {filename && (
×
153
            <Text className="font-mono text-sm text-neutral-200" numberOfLines={1}>
154
              {filename}
155
            </Text>
156
          )}
157
        </View>
158
        <View className="flex-row items-center gap-2">
159
          {additions > 0 && <Text className="font-mono text-xs text-teal-400">+{additions}</Text>}
×
160
          {deletions > 0 && <Text className="font-mono text-xs text-coral-400">-{deletions}</Text>}
×
161
        </View>
162
      </Pressable>
163

164
      {/* Content */}
165
      {!collapsed && (
×
166
        <ScrollView horizontal showsHorizontalScrollIndicator={false}>
167
          <View className="min-w-full">
168
            {lines.map((line, index) => (
NEW
169
              <View key={`${line.type}-${index}`} className={`flex-row ${getLineStyle(line.type)}`}>
×
170
                {showLineNumbers && (
×
171
                  <View className="flex-row">
172
                    <Text className="font-mono text-xs text-neutral-600 w-10 text-right px-2 py-1 bg-neutral-900/50">
173
                      {line.oldLineNumber || ''}
×
174
                    </Text>
175
                    <Text className="font-mono text-xs text-neutral-600 w-10 text-right px-2 py-1 bg-neutral-900/50">
176
                      {line.newLineNumber || ''}
×
177
                    </Text>
178
                  </View>
179
                )}
180
                <Text className={`font-mono text-sm w-4 py-1 ${getPrefixColor(line.type)}`}>
181
                  {getLinePrefix(line.type)}
182
                </Text>
183
                <Text
184
                  className="font-mono text-sm text-neutral-200 py-1 pr-4 flex-1"
185
                  numberOfLines={1}
186
                >
187
                  {line.content}
188
                </Text>
189
              </View>
190
            ))}
191
          </View>
192
        </ScrollView>
193
      )}
194
    </View>
195
  );
196
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc