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

agentic-dev-library / thumbcode / 21119927882

18 Jan 2026 10:47PM UTC coverage: 25.367% (-0.2%) from 25.529%
21119927882

Pull #57

github

web-flow
Merge 865a021e3 into 1c5d35a49
Pull Request #57: Implement Comprehensive Accessibility Features

260 of 1630 branches covered (15.95%)

Branch coverage included in aggregate %.

0 of 4 new or added lines in 3 files covered. (0.0%)

2 existing lines in 2 files now uncovered.

621 of 1843 relevant lines covered (33.7%)

1.49 hits per line

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

0.0
/src/components/code/FileTree.tsx
1
/**
2
 * FileTree Component
3
 *
4
 * Displays a hierarchical file/folder structure.
5
 * Supports expandable folders and file type icons.
6
 */
7

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

11
interface FileNode {
12
  name: string;
13
  type: 'file' | 'folder';
14
  path: string;
15
  children?: FileNode[];
16
  modified?: boolean;
17
  added?: boolean;
18
  deleted?: boolean;
19
}
20

21
interface FileTreeProps {
22
  /** Root nodes of the tree */
23
  data: FileNode[];
24
  /** Callback when a file is selected */
25
  onSelectFile?: (path: string) => void;
26
  /** Currently selected file path */
27
  selectedPath?: string;
28
  /** Initially expanded folders */
29
  defaultExpanded?: string[];
30
  /** Show file status indicators */
31
  showStatus?: boolean;
32
}
33

34
interface FileTreeNodeProps {
35
  node: FileNode;
36
  depth: number;
37
  onSelectFile?: (path: string) => void;
38
  selectedPath?: string;
39
  expandedPaths: Set<string>;
40
  toggleExpanded: (path: string) => void;
41
  showStatus?: boolean;
42
}
43

44
function getFileIcon(name: string): string {
45
  const ext = name.split('.').pop()?.toLowerCase();
×
46
  const iconMap: Record<string, string> = {
×
47
    ts: '📘',
48
    tsx: '⚛️',
49
    js: '📙',
50
    jsx: '⚛️',
51
    json: '📋',
52
    md: '📝',
53
    css: '🎨',
54
    scss: '🎨',
55
    html: '🌐',
56
    png: '🖼️',
57
    jpg: '🖼️',
58
    svg: '📐',
59
    git: '🔧',
60
    env: '⚙️',
61
    lock: '🔒',
62
  };
63
  return iconMap[ext || ''] || '📄';
×
64
}
65

66
function FileTreeNode({
67
  node,
68
  depth,
69
  onSelectFile,
70
  selectedPath,
71
  expandedPaths,
72
  toggleExpanded,
73
  showStatus,
74
}: FileTreeNodeProps) {
75
  const isExpanded = expandedPaths.has(node.path);
×
76
  const isSelected = selectedPath === node.path;
×
77
  const isFolder = node.type === 'folder';
×
78
  const hasChildren = node.children && node.children.length > 0;
×
79

80
  const icon = isFolder ? (isExpanded ? '📂' : '📁') : getFileIcon(node.name);
×
81

82
  const statusColor = node.added
×
83
    ? 'text-teal-400'
84
    : node.modified
×
85
      ? 'text-gold-400'
86
      : node.deleted
×
87
        ? 'text-coral-400'
88
        : '';
89

NEW
90
  const accessibilityLabel = `${node.name}, ${
×
91
    isFolder ? 'folder' : 'file'
×
92
  }, ${
93
    node.added ? 'added' : node.modified ? 'modified' : node.deleted ? 'deleted' : ''
×
94
  }`;
NEW
95
  const accessibilityHint = isFolder
×
96
    ? isExpanded
×
97
      ? 'Collapse folder'
98
      : 'Expand folder'
99
    : 'Open file';
100

UNCOV
101
  return (
×
102
    <View>
103
      <Pressable
104
        accessibilityRole="button"
105
        accessibilityLabel={accessibilityLabel}
106
        accessibilityHint={accessibilityHint}
107
        onPress={() => {
108
          if (isFolder && hasChildren) {
×
109
            toggleExpanded(node.path);
×
110
          } else if (!isFolder) {
×
111
            onSelectFile?.(node.path);
×
112
          }
113
        }}
114
        className={`flex-row items-center py-1.5 px-2 ${
115
          isSelected ? 'bg-teal-600/20' : 'active:bg-neutral-700'
×
116
        }`}
117
        style={{ paddingLeft: 8 + depth * 16 }}
118
      >
119
        {isFolder && hasChildren && (
×
120
          <Text className="text-xs text-neutral-500 w-4 mr-1">{isExpanded ? '▼' : '▶'}</Text>
×
121
        )}
122
        {(!isFolder || !hasChildren) && <View className="w-4 mr-1" />}
×
123

124
        <Text className="mr-2">{icon}</Text>
125

126
        <Text
127
          className={`font-mono text-sm flex-1 ${
128
            isSelected ? 'text-teal-300' : 'text-neutral-200'
×
129
          } ${statusColor}`}
130
          numberOfLines={1}
131
        >
132
          {node.name}
133
        </Text>
134

135
        {showStatus && (node.added || node.modified || node.deleted) && (
×
136
          <Text className={`text-xs ${statusColor} ml-2`}>
137
            {node.added ? 'A' : node.modified ? 'M' : 'D'}
×
138
          </Text>
139
        )}
140
      </Pressable>
141

142
      {isFolder && isExpanded && hasChildren && (
×
143
        <View>
144
          {node.children?.map((child) => (
145
            <FileTreeNode
×
146
              key={child.path}
147
              node={child}
148
              depth={depth + 1}
149
              onSelectFile={onSelectFile}
150
              selectedPath={selectedPath}
151
              expandedPaths={expandedPaths}
152
              toggleExpanded={toggleExpanded}
153
              showStatus={showStatus}
154
            />
155
          ))}
156
        </View>
157
      )}
158
    </View>
159
  );
160
}
161

162
export function FileTree({
163
  data,
164
  onSelectFile,
165
  selectedPath,
166
  defaultExpanded = [],
×
167
  showStatus = true,
×
168
}: FileTreeProps) {
169
  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set(defaultExpanded));
×
170

171
  const toggleExpanded = (path: string) => {
×
172
    setExpandedPaths((prev) => {
×
173
      const next = new Set(prev);
×
174
      if (next.has(path)) {
×
175
        next.delete(path);
×
176
      } else {
177
        next.add(path);
×
178
      }
179
      return next;
×
180
    });
181
  };
182

183
  // Sort nodes: folders first, then alphabetically
184
  const sortedData = useMemo(() => {
×
185
    const sortNodes = (nodes: FileNode[]): FileNode[] => {
×
186
      return [...nodes]
×
187
        .sort((a, b) => {
188
          if (a.type !== b.type) {
×
189
            return a.type === 'folder' ? -1 : 1;
×
190
          }
191
          return a.name.localeCompare(b.name);
×
192
        })
193
        .map((node) => ({
×
194
          ...node,
195
          children: node.children ? sortNodes(node.children) : undefined,
×
196
        }));
197
    };
198
    return sortNodes(data);
×
199
  }, [data]);
200

201
  return (
×
202
    <View
203
      className="bg-surface overflow-hidden"
204
      style={{
205
        borderTopLeftRadius: 12,
206
        borderTopRightRadius: 10,
207
        borderBottomRightRadius: 14,
208
        borderBottomLeftRadius: 8,
209
      }}
210
    >
211
      {sortedData.map((node) => (
212
        <FileTreeNode
×
213
          key={node.path}
214
          node={node}
215
          depth={0}
216
          onSelectFile={onSelectFile}
217
          selectedPath={selectedPath}
218
          expandedPaths={expandedPaths}
219
          toggleExpanded={toggleExpanded}
220
          showStatus={showStatus}
221
        />
222
      ))}
223
    </View>
224
  );
225
}
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