임동욱 임동욱 08-18
feat: FoxDisclosure 추가
Layout 시안에서 유일하게 남아 있던 disclosure다. 아코디언과 겹쳐 보이지만
바탕·테두리·크기 축이 없고 캐럿이 제목 앞에 와서 시안이 별개로 뒀다.

Co-Authored-By: Claude Opus 5 
@64cce4b0a63cc3da1904b95fe45b9ee19c3888f3
 
@fox/core/components/fox-disclosure/fox-disclosure.tsx (added)
+++ @fox/core/components/fox-disclosure/fox-disclosure.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useId, useState, type ReactNode } from "react";
+import { cx } from "../../utils";
+import { FoxCaretCircleRightIcon, FoxCaretCircleUpIcon } from "../../icons";
+
+export interface FoxDisclosureProps {
+  title: ReactNode;
+  children?: ReactNode;
+  /** 주면 호출부가 열림을 소유한다. 없으면 컴포넌트가 스스로 여닫는다. */
+  open?: boolean;
+  defaultOpen?: boolean;
+  onOpenChange?: (open: boolean) => void;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  /** 배치 조정용. */
+  className?: string;
+}
+
+/**
+ * @fox 디스클로저. 제목 한 줄을 누르면 아래 내용이 열린다.
+ *
+ * FoxAccordion과 겹쳐 보이지만 시안이 따로 둔 별개 컴포넌트다 — 바탕·테두리·크기 축이
+ * 없고 캐럿이 제목 앞에 온다. 목록을 나누는 아코디언과 달리 글 사이에 끼우는 접기다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxDisclosure({
+  title,
+  children,
+  open,
+  defaultOpen = false,
+  onOpenChange,
+  hidden = false,
+  className,
+}: FoxDisclosureProps) {
+  const autoId = useId();
+  const [inner, setInner] = useState(defaultOpen);
+
+  if (hidden) {
+    return null;
+  }
+
+  const expanded = open ?? inner;
+  const panelId = `${autoId}-panel`;
+
+  const toggle = () => {
+    if (open === undefined) {
+      setInner(!expanded);
+    }
+    onOpenChange?.(!expanded);
+  };
+
+  return (
+    <div
+      className={cx(
+        "fox-disclosure",
+        expanded && "fox-disclosure--open",
+        className
+      )}
+    >
+      <button
+        className="fox-disclosure__tit"
+        type="button"
+        aria-expanded={expanded}
+        aria-controls={panelId}
+        onClick={toggle}
+      >
+        <span className="fox-disclosure__icon" aria-hidden="true">
+          {expanded ? <FoxCaretCircleUpIcon /> : <FoxCaretCircleRightIcon />}
+        </span>
+        <span className="fox-disclosure__title">{title}</span>
+      </button>
+
+      {/* 닫혔을 때는 DOM에서 뺀다 — 안의 초점 가능한 것들이 탭 순서에 남으면 안 된다. */}
+      {expanded && (
+        <div className="fox-disclosure__contents" id={panelId}>
+          {children}
+        </div>
+      )}
+    </div>
+  );
+}
 
@fox/core/components/fox-disclosure/index.ts (added)
+++ @fox/core/components/fox-disclosure/index.ts
@@ -0,0 +1,1 @@
+export { FoxDisclosure, type FoxDisclosureProps } from "./fox-disclosure";
@fox/core/components/index.ts
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
@@ -21,6 +21,7 @@
 export * from "./fox-chip-area";
 export * from "./fox-chip-select-option";
 export * from "./fox-conditional-option";
+export * from "./fox-disclosure";
 export * from "./fox-email";
 export * from "./fox-file-upload";
 export * from "./fox-form-label";
@fox/dev-test/component-registry.tsx
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
@@ -60,6 +60,7 @@
 import { FoxModal } from "../core/components/fox-modal";
 import { FoxAccordion } from "../core/components/fox-accordion";
 import { FoxCard } from "../core/components/fox-card";
+import { FoxDisclosure } from "../core/components/fox-disclosure";
 import styles from "./dev-test.module.scss";
 
 export interface ComponentExample {
@@ -3464,6 +3465,35 @@
     ],
   },
   {
+    id: "fox-disclosure",
+    name: "FoxDisclosure",
+    description:
+      "Figma 시안(disclosure 391:4082 닫힘 · 391:4083 열림)을 옮겼습니다. 캐럿이 제목 앞에 오고 바탕도 테두리도 없어, 글 사이에 끼우는 가벼운 접기입니다 — 목록을 나누는 FoxAccordion과는 다른 컴포넌트입니다. 시안에 크기 축도 계열 축도 없어 변형이 없고 상태는 닫힘·열림 둘뿐입니다(캐럿이 오른쪽 → 위로 바뀝니다). 열림을 밖에서 쥘 수 있고 안 넘기면 스스로 여닫습니다. 닫히면 내용이 DOM에서 빠져 탭 순서에 남지 않습니다.",
+    variants: [
+      {
+        label: "시안 그대로 — 닫힘 · 열림",
+        node: (
+          <div className={styles.demoStack}>
+            <FoxDisclosure title="디스클로저 레이블" />
+            <FoxDisclosure title="디스클로저 레이블" defaultOpen>
+              <p>펼쳐진 내용이 들어가는 자리입니다.</p>
+            </FoxDisclosure>
+          </div>
+        ),
+      },
+      {
+        label: "내용은 슬롯이라 무엇이든 놓입니다",
+        node: (
+          <div className={styles.demoStack}>
+            <FoxDisclosure title="첨부된 그림 보기" defaultOpen>
+              <DemoThumb />
+            </FoxDisclosure>
+          </div>
+        ),
+      },
+    ],
+  },
+  {
     id: "fox-card",
     name: "FoxCard",
     description:
 
@fox/styles/_fox-disclosure.scss (added)
+++ @fox/styles/_fox-disclosure.scss
@@ -0,0 +1,66 @@
+// FoxDisclosure — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                       disclosure (391:4082 닫힘 · 391:4083 열림 · 섹션 391:4102)
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-disclosure fox-disclosure--open">
+//     <button class="fox-disclosure__tit" type="button" aria-expanded="true" aria-controls="…">
+//       <span class="fox-disclosure__icon">…svg…</span>
+//       <span class="fox-disclosure__title">제목</span>
+//     </button>
+//     <div class="fox-disclosure__contents" id="…">…</div>          <!-- 열렸을 때만 -->
+//   </div>
+//
+// 캐럿이 제목 앞에 오고 바탕·테두리가 없다. 시안에 크기 축도 계열 축도 없어 변형이 없다.
+//
+// 토큰이 아닌 값:
+//   - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로
+//     export하지 않는다.
+//   - 버튼 초기화(appearance/cursor/색 상속): 구조 규칙.
+
+@use "abstracts" as fox;
+
+.fox-disclosure {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: fox.gap(3);
+  inline-size: 100%;
+
+  // 제목 줄은 통째로 누르는 자리다. 글자 폭만 차지한다 — 시안이 hug다.
+  &__tit {
+    display: flex;
+    align-items: center;
+    gap: fox.gap(2);
+    margin: 0;
+    padding: 0;
+    border: none;
+    background: none;
+    text-align: start;
+    cursor: pointer;
+  }
+
+  &__icon {
+    display: inline-flex;
+    flex-shrink: 0;
+    inline-size: fox.icon(4);
+    block-size: fox.icon(4);
+    color: fox.color(element-neutral-strong);
+  }
+
+  &__title {
+    color: fox.color(font-neutral-strong);
+    font-family: fox.font-family(body);
+    font-size: fox.font-size(body-md);
+    font-weight: fox.font-weight(regular);
+    line-height: 1.5;
+    letter-spacing: -0.025em;
+    word-break: break-word;
+  }
+
+  &__contents {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    inline-size: 100%;
+  }
+}
@fox/styles/components.scss
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
@@ -58,3 +58,4 @@
 @use "fox-list-search";
 @use "fox-list-toolbar";
 @use "fox-list-container";
+@use "fox-disclosure";
Add a comment
List