File name
Commit message
Commit date
File name
Commit message
Commit date
import type { ReactNode } from "react";
interface SidebarGroupProps {
label: string;
icon?: ReactNode;
/** 펼침 상태 — 상태 소유는 소비처(feature)다. 이 컴포넌트는 비제어 프레젠테이션이다. */
isExpanded: boolean;
onToggle: () => void;
/** 자식 SidebarLink들(isNested로 들여쓰기). */
children: ReactNode;
}
/**
* 사이드바 그룹 헤딩 — 자식 내비게이션 항목(SidebarLink)을 펼침/접힘으로 묶는다.
* 상태를 갖지 않는 프레젠테이션 컴포넌트로, IconButton·SidebarLink와 동일한 관례로
* 'use client'를 붙이지 않는다: onToggle을 받아 <button onClick>에 연결하지만,
* 이 파일 자체는 훅을 쓰지 않는 순수 함수 컴포넌트다. Next.js App Router의 클라이언트
* 경계는 "이 모듈을 어느 파일이 import하는가"로 정해지므로, 실제 펼침 상태(useState 등)를
* 들고 있는 소비처(feature) 파일이 'use client'면 이 컴포넌트도 그 클라이언트 모듈
* 그래프에 포함되어 정상 동작한다(node_modules/next/dist/docs/01-app/01-getting-started/
* 05-server-and-client-components.md "module graph" 설명 참조).
*
* aria-controls는 의도적으로 생략했다 — 대상 id를 안전하게 만들 방법이 마땅치 않다
* (useId는 위 이유로 이 컴포넌트에서 쓸 수 없고, label을 id로 슬러그화하면 공백 포함
* 라벨이나 중복 라벨에서 깨질 수 있다). WAI-ARIA APG의 Disclosure 패턴에서 aria-controls는
* 필수가 아니라 선택 사항이며(스크린리더 지원도 일관적이지 않음), aria-expanded + 버튼
* 바로 다음에 내용이 오는 DOM 인접 구조만으로 충분하다고 판단했다.
*/
export function SidebarGroup({ label, icon, isExpanded, onToggle, children }: SidebarGroupProps) {
return (
<div>
<button
type="button"
onClick={onToggle}
aria-expanded={isExpanded}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-label-lg text-foreground transition-colors duration-200 hover:bg-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
>
{icon}
{/* min-w-0: SidebarLink와 동일한 truncate 대응(flex 아이템 기본 min-width:auto가
overflow-hidden을 무력화하는 문제). 접힘 폭(md:w-16)에서도 한 줄 유지 + 말줄임만
적용한다 — 아이콘 전용 표시로 전환하는 정식 해법은 아이콘 자산이 없어 이번
범위에서 다루지 않는다(SidebarLink와 동일 사유). */}
<span className="min-w-0 flex-1 truncate">{label}</span>
<ChevronIcon
className={`shrink-0 transition-transform duration-200 ${isExpanded ? "rotate-90" : "rotate-0"}`}
/>
</button>
{isExpanded && <div className="flex flex-col">{children}</div>}
</div>
);
}
/**
* 펼침/접힘 표시용 셰브론(오른쪽 화살표 — 펼치면 90도 회전해 아래를 향한다). 그룹 헤딩
* 전용 장식 요소라 이 파일 안에 둔다.
*
* ⚠️ 손으로 그린 마지막 아이콘이다. 디자인시스템에 같은 그림(`FoxCaretRightIcon`)이 있지만,
* 회전(`rotate-90`)과 크기(`size-4`)를 Tailwind 클래스에 기대고 있어 그 두 개를 실재하는
* CSS로 옮기기 전에는 바꿔도 나아지지 않는다 — components/ui 전체 리스타일과 함께 처리한다.
*/
function ChevronIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={`size-4 ${className ?? ""}`}
>
<path d="M9 6l6 6-6 6" />
</svg>
);
}