Group changed files in the Changes view into a collapsible directory tree instead of a flat list, on every form factor — consistent with the Files explorer, which already renders a directory tree on mobile. Folders are collapsible and expanded by default, single-child directory chains are compressed into one row (packages/app/src), and folder rows show aggregate +/- stats. Client-only, built entirely from the existing ParsedDiffFile paths: - diff-tree.ts: pure build/compress/flatten of the directory hierarchy, keyed by full uncompressed dir path so collapse state survives compression changes as the diff mutates. - diff-flat-items.ts: pure buildDiffFlatItems + sumHeightsBefore. The function retains a flat mode (showTree=false) as a tested seam for a possible future flat/tree toggle; sticky header indices are file-headers-only, derived from the post-collapse list. - panel-store: diffCollapsedFoldersByWorkspace slice (collapsed-set, so folders default to expanded), persisted via version 12. The set is reconciled against the current diff so folders that leave and reappear default to expanded and the stored set stays bounded. Presentation primitives (chevron, indent guides, indent constant) are extracted into components/tree-primitives and shared with the Files explorer, which drops its private copies (and a banned useUnistyles call site). The two trees now render identical rows; only the tree-building stays separate (server listings vs. diff paths). The toolbar's expand-all stays file-diffs-only (renamed allFileDiffsExpanded); a separate control collapses/expands all folders. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
import { useMemo } from "react";
|
|
import { View } from "react-native";
|
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
import { ChevronRight } from "lucide-react-native";
|
|
import { SPACING, type Theme } from "@/styles/theme";
|
|
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
|
|
|
// Shared presentation primitives for the app's directory trees. Both the Files
|
|
// explorer (server-loaded listings) and the Changes view (client-built from diff
|
|
// paths) render different data, but their ROWS should look identical — same
|
|
// indentation, guide lines, and chevron. Keep those here so the two trees can't
|
|
// drift apart.
|
|
export const TREE_INDENT_PER_LEVEL = 16;
|
|
|
|
/** Left padding for a tree row at `depth`. Shared by folder rows and file headers
|
|
* in the Changes tree so their indentation can't drift apart. */
|
|
export function treeRowPaddingLeft(depth: number): number {
|
|
return SPACING[3] + depth * TREE_INDENT_PER_LEVEL;
|
|
}
|
|
|
|
const foregroundMutedIconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
|
|
|
const ThemedChevronRight = withUnistyles(ChevronRight);
|
|
|
|
/**
|
|
* Vertical guide lines connecting nested rows to their ancestors — one line per
|
|
* ancestor depth level, positioned absolutely within the (relative) row. Renders
|
|
* nothing at depth 0.
|
|
*/
|
|
export function TreeIndentGuides({ depth }: { depth: number }) {
|
|
const guides = useMemo(
|
|
() =>
|
|
Array.from({ length: depth }, (_, index) => ({
|
|
key: index,
|
|
style: [
|
|
styles.indentGuide,
|
|
inlineUnistylesStyle({ left: SPACING[3] + index * TREE_INDENT_PER_LEVEL + 4 }),
|
|
],
|
|
})),
|
|
[depth],
|
|
);
|
|
return (
|
|
<>
|
|
{guides.map((guide) => (
|
|
<View key={guide.key} style={guide.style} pointerEvents="none" />
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/** Rotating disclosure chevron for a directory row (points right; rotates down when expanded). */
|
|
export function TreeChevron({ expanded }: { expanded: boolean }) {
|
|
return (
|
|
<View style={expanded ? CHEVRON_EXPANDED_STYLE : styles.chevron}>
|
|
<ThemedChevronRight size={16} uniProps={foregroundMutedIconColorMapping} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme: Theme) => ({
|
|
indentGuide: {
|
|
position: "absolute",
|
|
top: 0,
|
|
bottom: 0,
|
|
width: 1,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
chevron: {
|
|
width: 16,
|
|
height: 16,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
flexShrink: 0,
|
|
},
|
|
chevronExpanded: {
|
|
transform: [{ rotate: "90deg" }],
|
|
},
|
|
}));
|
|
|
|
// Stable module-level style ref so TreeChevron passes a constant array, not one created
|
|
// per render — satisfies react-perf (no inline-array prop) without a per-render useMemo.
|
|
const CHEVRON_EXPANDED_STYLE = [styles.chevron, styles.chevronExpanded];
|