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

agentic-dev-library / thumbcode / 21125784924

19 Jan 2026 04:54AM UTC coverage: 21.27% (-1.2%) from 22.502%
21125784924

Pull #71

github

web-flow
Merge 766eb9c8a into 115c7bec6
Pull Request #71: feat(brand): implement procedural paint daube SVG icon system

255 of 1804 branches covered (14.14%)

Branch coverage included in aggregate %.

0 of 233 new or added lines in 11 files covered. (0.0%)

2 existing lines in 2 files now uncovered.

646 of 2432 relevant lines covered (26.56%)

1.58 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 { useMemo, useState } from 'react';
10
import { Pressable, View } from 'react-native';
11
import {
12
  FileCodeIcon,
13
  FileConfigIcon,
14
  FileDataIcon,
15
  FileDocIcon,
16
  FileIcon,
17
  FileMediaIcon,
18
  FileStyleIcon,
19
  FileWebIcon,
20
  FolderIcon,
21
  FolderOpenIcon,
22
  type IconColor,
23
} from '@/components/icons';
24
import { Text } from '@/components/ui';
25

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

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

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

59
/** File icon component type */
60
type FileIconComponent = React.FC<{ size?: number; color?: IconColor; turbulence?: number }>;
61

62
interface FileIconInfo {
63
  Icon: FileIconComponent;
64
  color: IconColor;
65
}
66

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

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

100
function getStatusText(node: FileNode): string {
101
  if (node.added) return 'added';
×
102
  if (node.modified) return 'modified';
×
103
  if (node.deleted) return 'deleted';
×
104
  return '';
×
105
}
106

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

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

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

152
  const rowClass = isSelected ? 'bg-teal-600/20' : 'active:bg-neutral-700';
×
153
  const textClass = isSelected ? 'text-teal-300' : 'text-neutral-200';
×
154

155
  return (
×
156
    <Pressable
157
      onPress={onPress}
158
      accessibilityRole="button"
159
      accessibilityLabel={accessibilityLabel}
160
      accessibilityHint={accessibilityHint}
161
      className={`flex-row items-center py-1.5 px-2 ${rowClass}`}
162
      style={{ paddingLeft: 8 + depth * 16 }}
163
    >
164
      {isFolder && hasChildren ? (
×
165
        <Text className="text-xs text-neutral-500 w-4 mr-1">{isExpanded ? '▼' : '▶'}</Text>
×
166
      ) : (
167
        <View className="w-4 mr-1" />
168
      )}
169
      <View className="mr-2">
170
        <iconInfo.Icon size={16} color={iconInfo.color} turbulence={0.15} />
171
      </View>
172
      <Text className={`font-mono text-sm flex-1 ${textClass} ${statusColor}`} numberOfLines={1}>
173
        {node.name}
174
      </Text>
175
    </Pressable>
176
  );
177
}
178

179
function FileTreeNode({
180
  node,
181
  depth,
182
  onSelectFile,
183
  selectedPath,
184
  expandedPaths,
185
  toggleExpanded,
186
  showStatus,
187
}: FileTreeNodeProps) {
188
  const isExpanded = expandedPaths.has(node.path);
×
189
  const isSelected = selectedPath === node.path;
×
190
  const isFolder = node.type === 'folder';
×
191
  const hasChildren = Boolean(node.children?.length);
×
192
  const statusColor = getStatusColor(node);
×
193

194
  const handlePress = () => {
×
195
    if (isFolder && hasChildren) {
×
196
      toggleExpanded(node.path);
×
197
    } else if (!isFolder) {
×
198
      onSelectFile?.(node.path);
×
199
    }
200
  };
201

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

204
  // Accessibility labels for screen readers
205
  const accessibilityLabel = [node.name, isFolder ? 'folder' : 'file', getStatusText(node)]
×
206
    .filter(Boolean)
207
    .join(', ');
208

209
  return (
×
210
    <View>
211
      <View className="flex-row items-center">
212
        <View className="flex-1">
213
          <FileTreeNodeRow
214
            node={node}
215
            depth={depth}
216
            isSelected={isSelected}
217
            isExpanded={isExpanded}
218
            isFolder={isFolder}
219
            hasChildren={hasChildren}
220
            statusColor={statusColor}
221
            onPress={handlePress}
222
            accessibilityLabel={accessibilityLabel}
223
            accessibilityHint={getAccessibilityHint(isFolder, hasChildren, isExpanded)}
224
          />
225
        </View>
226
        {shouldShowStatus && (
×
227
          <Text className={`text-xs ${statusColor} mr-2`}>{getStatusLabel(node)}</Text>
228
        )}
229
      </View>
230

231
      {isFolder && isExpanded && hasChildren && (
×
232
        <View>
233
          {node.children?.map((child) => (
234
            <FileTreeNode
×
235
              key={child.path}
236
              node={child}
237
              depth={depth + 1}
238
              onSelectFile={onSelectFile}
239
              selectedPath={selectedPath}
240
              expandedPaths={expandedPaths}
241
              toggleExpanded={toggleExpanded}
242
              showStatus={showStatus}
243
            />
244
          ))}
245
        </View>
246
      )}
247
    </View>
248
  );
249
}
250

251
export function FileTree({
252
  data,
253
  onSelectFile,
254
  selectedPath,
255
  defaultExpanded = [],
×
256
  showStatus = true,
×
257
}: FileTreeProps) {
258
  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set(defaultExpanded));
×
259

260
  const toggleExpanded = (path: string) => {
×
261
    setExpandedPaths((prev) => {
×
262
      const next = new Set(prev);
×
263
      if (next.has(path)) {
×
264
        next.delete(path);
×
265
      } else {
266
        next.add(path);
×
267
      }
268
      return next;
×
269
    });
270
  };
271

272
  // Sort nodes: folders first, then alphabetically
273
  const sortedData = useMemo(() => {
×
274
    const sortNodes = (nodes: FileNode[]): FileNode[] => {
×
275
      return [...nodes]
×
276
        .sort((a, b) => {
277
          if (a.type !== b.type) {
×
278
            return a.type === 'folder' ? -1 : 1;
×
279
          }
280
          return a.name.localeCompare(b.name);
×
281
        })
282
        .map((node) => ({
×
283
          ...node,
284
          children: node.children ? sortNodes(node.children) : undefined,
×
285
        }));
286
    };
287
    return sortNodes(data);
×
288
  }, [data]);
289

290
  return (
×
291
    <View
292
      accessibilityRole="list"
293
      accessibilityLabel="File tree"
294
      className="bg-surface overflow-hidden"
295
      style={{
296
        borderTopLeftRadius: 12,
297
        borderTopRightRadius: 10,
298
        borderBottomRightRadius: 14,
299
        borderBottomLeftRadius: 8,
300
      }}
301
    >
302
      {sortedData.map((node) => (
303
        <FileTreeNode
×
304
          key={node.path}
305
          node={node}
306
          depth={0}
307
          onSelectFile={onSelectFile}
308
          selectedPath={selectedPath}
309
          expandedPaths={expandedPaths}
310
          toggleExpanded={toggleExpanded}
311
          showStatus={showStatus}
312
        />
313
      ))}
314
    </View>
315
  );
316
}
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