Jiseoup/showmycodePublic
EN|KO
  • 코드
  • 커밋
  • 풀 리퀘스트
← 목록으로
Merged

feat: toggle file tree expand/collapse with state-aware button

#10
JiseoupJiseoup · 2026년 5월 8일feat/file-tree-expand-collapse-toggle → main
feat
개요커밋변경된 파일
변경된 파일

+133 -48 · 5개

@@ -28,7 +28,8 @@ export default async function CodePage({ params, searchParams }: Props) {
repo={repo}
selectedPath={selectedPath}
lang={lang as Locale}
- filesLabel={dict.code.files}
+ expandAllLabel={dict.code.expandAll}
+ collapseAllLabel={dict.code.collapseAll}
branches={branchNames}
branch={branch}
/>
@@ -50,6 +50,23 @@ function buildTree(items: GhTreeItem[]): TreeNode[] {
return sort(root);
}
+// Walk the tree once to seed which directory paths should be open initially.
+function collectInitialOpen(
+ nodes: TreeNode[],
+ selectedPath: string | undefined,
+ depth: number,
+ defaultOpenDepth: number,
+ acc: Set<string>,
+): void {
+ for (const n of nodes) {
+ if (n.type !== "tree") continue;
+ const shouldOpen =
+ depth < defaultOpenDepth || (selectedPath?.startsWith(n.path + "/") ?? false);
+ if (shouldOpen) acc.add(n.path);
+ if (n.children) collectInitialOpen(n.children, selectedPath, depth + 1, defaultOpenDepth, acc);
+ }
+}
+
function TreeNodeItem({
node,
owner,
@@ -58,7 +75,8 @@ function TreeNodeItem({
selectedPath,
branch,
depth,
- defaultOpenDepth,
+ openPaths,
+ onToggle,
}: {
node: TreeNode;
owner: string;
@@ -67,17 +85,15 @@ function TreeNodeItem({
selectedPath?: string;
branch: string;
depth: number;
- defaultOpenDepth: number;
+ openPaths: Set<string>;
+ onToggle: (path: string) => void;
}) {
- const [open, setOpen] = useState(
- depth < defaultOpenDepth || (selectedPath?.startsWith(node.path + "/") ?? false),
- );
-
if (node.type === "tree") {
+ const open = openPaths.has(node.path);
return (
<li>
<button
- onClick={() => setOpen((o) => !o)}
+ onClick={() => onToggle(node.path)}
className="hover:bg-accent flex w-full items-center gap-1.5 rounded px-2 py-0.5 text-left text-sm transition-colors"
style={{ paddingLeft: `${8 + depth * 12}px` }}
>
@@ -109,7 +125,8 @@ function TreeNodeItem({
selectedPath={selectedPath}
branch={branch}
depth={depth + 1}
- defaultOpenDepth={defaultOpenDepth}
+ openPaths={openPaths}
+ onToggle={onToggle}
/>
))}
</ul>
@@ -148,6 +165,9 @@ export function FileTree({
selectedPath,
branch,
defaultOpenDepth = 0,
+ expandAllLabel,
+ collapseAllLabel,
+ branchSelector,
}: {
items: GhTreeItem[];
owner: string;
@@ -156,24 +176,88 @@ export function FileTree({
selectedPath?: string;
branch: string;
defaultOpenDepth?: number;
+ expandAllLabel: string;
+ collapseAllLabel: string;
+ branchSelector?: React.ReactNode;
}) {
const tree = buildTree(items);
+ const [openPaths, setOpenPaths] = useState<Set<string>>(() => {
+ const set = new Set<string>();
+ collectInitialOpen(tree, selectedPath, 0, defaultOpenDepth, set);
+ return set;
+ });
+
+ const handleToggle = (path: string) => {
+ setOpenPaths((prev) => {
+ const next = new Set(prev);
+ if (next.has(path)) next.delete(path);
+ else next.add(path);
+ return next;
+ });
+ };
+
+ const allExpanded = openPaths.size === 0;
+ const expandAll = () =>
+ setOpenPaths(new Set(items.filter((i) => i.type === "tree").map((i) => i.path)));
+ const collapseAll = () => setOpenPaths(new Set());
+ const toggleAllLabel = allExpanded ? expandAllLabel : collapseAllLabel;
return (
- <ul className="space-y-0.5 text-sm">
- {tree.map((node) => (
- <TreeNodeItem
- key={node.path}
- node={node}
- owner={owner}
- repo={repo}
- lang={lang}
- selectedPath={selectedPath}
- branch={branch}
- depth={0}
- defaultOpenDepth={defaultOpenDepth}
- />
- ))}
- </ul>
+ <div className="flex h-full flex-col overflow-hidden">
+ <div className="border-border space-y-2 border-b px-3 py-2.5">
+ <div className="flex items-center justify-between gap-2">
+ <span
+ className="text-foreground truncate text-xs font-semibold tracking-wide uppercase"
+ title={repo}
+ >
+ {repo}
+ </span>
+ <button
+ type="button"
+ onClick={allExpanded ? expandAll : collapseAll}
+ title={toggleAllLabel}
+ aria-label={toggleAllLabel}
+ className="text-muted-foreground hover:text-foreground hover:bg-accent shrink-0 rounded p-1 transition-colors"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 16 16"
+ fill="currentColor"
+ className="h-4 w-4"
+ aria-hidden
+ >
+ <path fillRule="evenodd" clipRule="evenodd" d="M9 9H4v1h5V9z" />
+ {allExpanded && (
+ <path fillRule="evenodd" clipRule="evenodd" d="M7 12V7H6v5h1z" />
+ )}
+ <path
+ fillRule="evenodd"
+ clipRule="evenodd"
+ d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z"
+ />
+ </svg>
+ </button>
+ </div>
+ {branchSelector}
+ </div>
+ <div className="flex-1 overflow-y-auto p-2">
+ <ul className="space-y-0.5 text-sm">
+ {tree.map((node) => (
+ <TreeNodeItem
+ key={node.path}
+ node={node}
+ owner={owner}
+ repo={repo}
+ lang={lang}
+ selectedPath={selectedPath}
+ branch={branch}
+ depth={0}
+ openPaths={openPaths}
+ onToggle={handleToggle}
+ />
+ ))}
+ </ul>
+ </div>
+ </div>
);
}
@@ -7,7 +7,8 @@ type Props = {
owner: string;
repo: string;
lang: string;
- filesLabel: string;
+ expandAllLabel: string;
+ collapseAllLabel: string;
selectedPath?: string;
branches: string[];
branch: string;
@@ -17,7 +18,8 @@ export async function Sidebar({
owner,
repo,
lang,
- filesLabel,
+ expandAllLabel,
+ collapseAllLabel,
selectedPath,
branches,
branch,
@@ -26,27 +28,21 @@ export async function Sidebar({
const defaultOpenDepth = Number(process.env.FILE_TREE_DEPTH) || 0;
return (
- <aside className="flex h-full flex-col overflow-hidden">
- <div className="border-border space-y-2 border-b px-3 py-2.5">
- <p className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
- {filesLabel}
- </p>
- <Suspense>
+ <FileTree
+ items={tree}
+ owner={owner}
+ repo={repo}
+ lang={lang}
+ selectedPath={selectedPath}
+ branch={branch}
+ defaultOpenDepth={defaultOpenDepth}
+ expandAllLabel={expandAllLabel}
+ collapseAllLabel={collapseAllLabel}
+ branchSelector={
+ <Suspense key="branch-selector" fallback={null}>
<BranchSelector branches={branches} current={branch} />
</Suspense>
- </div>
-
- <div className="flex-1 overflow-y-auto p-2">
- <FileTree
- items={tree}
- owner={owner}
- repo={repo}
- lang={lang}
- selectedPath={selectedPath}
- branch={branch}
- defaultOpenDepth={defaultOpenDepth}
- />
- </div>
- </aside>
+ }
+ />
);
}
@@ -13,7 +13,9 @@
"code": {
"files": "Files",
"branch": "Branch",
- "selectFile": "Select a file from the left"
+ "selectFile": "Select a file from the left",
+ "expandAll": "Expand All",
+ "collapseAll": "Collapse All"
},
"commits": {
"title": "Commit History",
@@ -13,7 +13,9 @@
"code": {
"files": "파일",
"branch": "브랜치",
- "selectFile": "왼쪽에서 파일을 선택하세요"
+ "selectFile": "왼쪽에서 파일을 선택하세요",
+ "expandAll": "모두 펼치기",
+ "collapseAll": "모두 접기"
},
"commits": {
"title": "커밋 히스토리",