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

agentic-dev-library / thumbcode / 21933635344

12 Feb 2026 04:31AM UTC coverage: 28.282% (-0.09%) from 28.372%
21933635344

Pull #120

github

web-flow
Merge 85853f9b5 into 82c88cdf1
Pull Request #120: fix(quality): SonarCloud bug, code smells, Readonly props

388 of 2123 branches covered (18.28%)

Branch coverage included in aggregate %.

1 of 40 new or added lines in 9 files covered. (2.5%)

2 existing lines in 2 files now uncovered.

1038 of 2919 relevant lines covered (35.56%)

8.06 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
 * Uses paint daube icons for brand consistency.
7
 */
8

9
import type React from 'react';
10
import { useMemo, useState } from 'react';
11
import { Pressable, View } from 'react-native';
12
import {
13
  ChevronDownIcon,
14
  FileCodeIcon,
15
  FileConfigIcon,
16
  FileDataIcon,
17
  FileDocIcon,
18
  FileIcon,
19
  FileMediaIcon,
20
  FileStyleIcon,
21
  FileWebIcon,
22
  FolderIcon,
23
  FolderOpenIcon,
24
  type IconColor,
25
} from '@/components/icons';
26
import { Text } from '@/components/ui';
27
import { organicBorderRadius } from '@/lib/organic-styles';
28

29
export interface FileNode {
30
  name: string;
31
  type: 'file' | 'folder';
32
  path: string;
33
  children?: FileNode[];
34
  modified?: boolean;
35
  added?: boolean;
36
  deleted?: boolean;
37
}
38

39
interface FileTreeProps {
40
  /** Root nodes of the tree */
41
  data: FileNode[];
42
  /** Callback when a file is selected */
43
  onSelectFile?: (path: string) => void;
44
  /** Currently selected file path */
45
  selectedPath?: string;
46
  /** Initially expanded folders */
47
  defaultExpanded?: string[];
48
  /** Show file status indicators */
49
  showStatus?: boolean;
50
}
51

52
interface FileTreeNodeProps {
53
  node: FileNode;
54
  depth: number;
55
  onSelectFile?: (path: string) => void;
56
  selectedPath?: string;
57
  expandedPaths: Set<string>;
58
  toggleExpanded: (path: string) => void;
59
  showStatus?: boolean;
60
}
61

62
/** File icon component type */
63
type FileIconComponent = React.FC<{ size?: number; color?: IconColor; turbulence?: number }>;
64

65
interface FileIconInfo {
66
  Icon: FileIconComponent;
67
  color: IconColor;
68
}
69

70
function getFileIconInfo(name: string): FileIconInfo {
71
  const ext = name.split('.').pop()?.toLowerCase();
×
72
  const iconMap: Record<string, FileIconInfo> = {
×
73
    ts: { Icon: FileCodeIcon, color: 'teal' },
74
    tsx: { Icon: FileCodeIcon, color: 'teal' },
75
    js: { Icon: FileCodeIcon, color: 'gold' },
76
    jsx: { Icon: FileCodeIcon, color: 'gold' },
77
    json: { Icon: FileDataIcon, color: 'gold' },
78
    md: { Icon: FileDocIcon, color: 'warmGray' },
79
    css: { Icon: FileStyleIcon, color: 'coral' },
80
    scss: { Icon: FileStyleIcon, color: 'coral' },
81
    html: { Icon: FileWebIcon, color: 'teal' },
82
    png: { Icon: FileMediaIcon, color: 'coral' },
83
    jpg: { Icon: FileMediaIcon, color: 'coral' },
84
    jpeg: { Icon: FileMediaIcon, color: 'coral' },
85
    svg: { Icon: FileMediaIcon, color: 'coral' },
86
    gif: { Icon: FileMediaIcon, color: 'coral' },
87
    git: { Icon: FileConfigIcon, color: 'warmGray' },
88
    env: { Icon: FileConfigIcon, color: 'warmGray' },
89
    lock: { Icon: FileConfigIcon, color: 'warmGray' },
90
    yaml: { Icon: FileConfigIcon, color: 'warmGray' },
91
    yml: { Icon: FileConfigIcon, color: 'warmGray' },
92
  };
93
  return iconMap[ext || ''] || { Icon: FileIcon, color: 'warmGray' };
×
94
}
95

96
function getStatusColor(node: FileNode): string {
97
  if (node.added) return 'text-teal-400';
×
98
  if (node.modified) return 'text-gold-400';
×
99
  if (node.deleted) return 'text-coral-400';
×
100
  return '';
×
101
}
102

103
function getStatusText(node: FileNode): string {
104
  if (node.added) return 'added';
×
105
  if (node.modified) return 'modified';
×
106
  if (node.deleted) return 'deleted';
×
107
  return '';
×
108
}
109

110
function getStatusLabel(node: FileNode): string {
111
  if (node.added) return 'A';
×
112
  if (node.modified) return 'M';
×
113
  if (node.deleted) return 'D';
×
114
  return '';
×
115
}
116

117
function getAccessibilityHint(
118
  isFolder: boolean,
119
  hasChildren: boolean,
120
  isExpanded: boolean
121
): string {
122
  if (!isFolder) return 'Open file';
×
123
  if (!hasChildren) return 'Empty folder';
×
124
  return isExpanded ? 'Collapse folder' : 'Expand folder';
×
125
}
126

127
function FileTreeNodeRow({
128
  node,
129
  depth,
130
  isSelected,
131
  isExpanded,
132
  isFolder,
133
  hasChildren,
134
  statusColor,
135
  onPress,
136
  accessibilityLabel,
137
  accessibilityHint,
138
}: {
139
  node: FileNode;
140
  depth: number;
141
  isSelected: boolean;
142
  isExpanded: boolean;
143
  isFolder: boolean;
144
  hasChildren: boolean;
145
  statusColor: string;
146
  onPress: () => void;
147
  accessibilityLabel: string;
148
  accessibilityHint: string;
149
}) {
150
  // Get the appropriate icon based on file type or folder state
151
  let iconInfo: FileIconInfo;
NEW
152
  if (isFolder) {
×
NEW
153
    const Icon = isExpanded ? FolderOpenIcon : FolderIcon;
×
NEW
154
    iconInfo = { Icon, color: 'gold' };
×
155
  } else {
NEW
156
    iconInfo = getFileIconInfo(node.name);
×
157
  }
158

159
  const rowClass = isSelected ? 'bg-teal-600/20' : 'active:bg-neutral-700';
×
160
  const textClass = isSelected ? 'text-teal-300' : 'text-neutral-200';
×
161

162
  return (
×
163
    <Pressable
164
      onPress={onPress}
165
      accessibilityRole="button"
166
      accessibilityLabel={accessibilityLabel}
167
      accessibilityHint={accessibilityHint}
168
      className={`flex-row items-center py-1.5 px-2 ${rowClass}`}
169
      style={{ paddingLeft: 8 + depth * 16 }}
170
    >
171
      <View className="w-4 mr-1 items-center justify-center">
172
        {isFolder && hasChildren ? (
×
173
          <View style={{ transform: [{ rotate: isExpanded ? '0deg' : '-90deg' }] }}>
×
174
            <ChevronDownIcon size={12} color="warmGray" turbulence={0.12} />
175
          </View>
176
        ) : null}
177
      </View>
178
      <View className="mr-2">
179
        <iconInfo.Icon size={16} color={iconInfo.color} turbulence={0.15} />
180
      </View>
181
      <Text className={`font-mono text-sm flex-1 ${textClass} ${statusColor}`} numberOfLines={1}>
182
        {node.name}
183
      </Text>
184
    </Pressable>
185
  );
186
}
187

188
function FileTreeNode({
189
  node,
190
  depth,
191
  onSelectFile,
192
  selectedPath,
193
  expandedPaths,
194
  toggleExpanded,
195
  showStatus,
196
}: Readonly<FileTreeNodeProps>) {
197
  const isExpanded = expandedPaths.has(node.path);
×
198
  const isSelected = selectedPath === node.path;
×
199
  const isFolder = node.type === 'folder';
×
200
  const hasChildren = Boolean(node.children?.length);
×
201
  const statusColor = getStatusColor(node);
×
202

203
  const handlePress = () => {
×
204
    if (isFolder && hasChildren) {
×
205
      toggleExpanded(node.path);
×
206
    } else if (!isFolder) {
×
207
      onSelectFile?.(node.path);
×
208
    }
209
  };
210

211
  const shouldShowStatus = showStatus && Boolean(node.added || node.modified || node.deleted);
×
212

213
  // Accessibility labels for screen readers
214
  const accessibilityLabel = [node.name, isFolder ? 'folder' : 'file', getStatusText(node)]
×
215
    .filter(Boolean)
216
    .join(', ');
217

218
  return (
×
219
    <View>
220
      <View className="flex-row items-center">
221
        <View className="flex-1">
222
          <FileTreeNodeRow
223
            node={node}
224
            depth={depth}
225
            isSelected={isSelected}
226
            isExpanded={isExpanded}
227
            isFolder={isFolder}
228
            hasChildren={hasChildren}
229
            statusColor={statusColor}
230
            onPress={handlePress}
231
            accessibilityLabel={accessibilityLabel}
232
            accessibilityHint={getAccessibilityHint(isFolder, hasChildren, isExpanded)}
233
          />
234
        </View>
235
        {shouldShowStatus && (
×
236
          <Text className={`text-xs ${statusColor} mr-2`}>{getStatusLabel(node)}</Text>
237
        )}
238
      </View>
239

240
      {isFolder && isExpanded && hasChildren && (
×
241
        <View>
242
          {node.children?.map((child) => (
243
            <FileTreeNode
×
244
              key={child.path}
245
              node={child}
246
              depth={depth + 1}
247
              onSelectFile={onSelectFile}
248
              selectedPath={selectedPath}
249
              expandedPaths={expandedPaths}
250
              toggleExpanded={toggleExpanded}
251
              showStatus={showStatus}
252
            />
253
          ))}
254
        </View>
255
      )}
256
    </View>
257
  );
258
}
259

260
export function FileTree({
261
  data,
262
  onSelectFile,
263
  selectedPath,
264
  defaultExpanded = [],
×
265
  showStatus = true,
×
266
}: Readonly<FileTreeProps>) {
267
  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set(defaultExpanded));
×
268

269
  const toggleExpanded = (path: string) => {
×
270
    setExpandedPaths((prev) => {
×
271
      const next = new Set(prev);
×
272
      if (next.has(path)) {
×
273
        next.delete(path);
×
274
      } else {
275
        next.add(path);
×
276
      }
277
      return next;
×
278
    });
279
  };
280

281
  // Sort nodes: folders first, then alphabetically
282
  const sortedData = useMemo(() => {
×
283
    const sortNodes = (nodes: FileNode[]): FileNode[] => {
×
284
      return [...nodes]
×
285
        .sort((a, b) => {
286
          if (a.type !== b.type) {
×
287
            return a.type === 'folder' ? -1 : 1;
×
288
          }
289
          return a.name.localeCompare(b.name);
×
290
        })
291
        .map((node) => ({
×
292
          ...node,
293
          children: node.children ? sortNodes(node.children) : undefined,
×
294
        }));
295
    };
296
    return sortNodes(data);
×
297
  }, [data]);
298

299
  return (
×
300
    <View
301
      accessibilityRole="list"
302
      accessibilityLabel="File tree"
303
      className="bg-surface overflow-hidden"
304
      style={organicBorderRadius.card}
305
    >
306
      {sortedData.map((node) => (
307
        <FileTreeNode
×
308
          key={node.path}
309
          node={node}
310
          depth={0}
311
          onSelectFile={onSelectFile}
312
          selectedPath={selectedPath}
313
          expandedPaths={expandedPaths}
314
          toggleExpanded={toggleExpanded}
315
          showStatus={showStatus}
316
        />
317
      ))}
318
    </View>
319
  );
320
}
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