feat: FoxDisclosure 추가
Layout 시안에서 유일하게 남아 있던 disclosure다. 아코디언과 겹쳐 보이지만 바탕·테두리·크기 축이 없고 캐럿이 제목 앞에 와서 시안이 별개로 뒀다. Co-Authored-By: Claude Opus 5
@64cce4b0a63cc3da1904b95fe45b9ee19c3888f3
+++ @fox/core/components/fox-disclosure/fox-disclosure.tsx
... | ... | @@ -0,0 +1,84 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useId, useState, type ReactNode } from "react"; | |
| 4 | +import { cx } from "../../utils"; | |
| 5 | +import { FoxCaretCircleRightIcon, FoxCaretCircleUpIcon } from "../../icons"; | |
| 6 | + | |
| 7 | +export interface FoxDisclosureProps { | |
| 8 | + title: ReactNode; | |
| 9 | + children?: ReactNode; | |
| 10 | + /** 주면 호출부가 열림을 소유한다. 없으면 컴포넌트가 스스로 여닫는다. */ | |
| 11 | + open?: boolean; | |
| 12 | + defaultOpen?: boolean; | |
| 13 | + onOpenChange?: (open: boolean) => void; | |
| 14 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 15 | + hidden?: boolean; | |
| 16 | + /** 배치 조정용. */ | |
| 17 | + className?: string; | |
| 18 | +} | |
| 19 | + | |
| 20 | +/** | |
| 21 | + * @fox 디스클로저. 제목 한 줄을 누르면 아래 내용이 열린다. | |
| 22 | + * | |
| 23 | + * FoxAccordion과 겹쳐 보이지만 시안이 따로 둔 별개 컴포넌트다 — 바탕·테두리·크기 축이 | |
| 24 | + * 없고 캐럿이 제목 앞에 온다. 목록을 나누는 아코디언과 달리 글 사이에 끼우는 접기다. | |
| 25 | + * | |
| 26 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 27 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 28 | + */ | |
| 29 | +export function FoxDisclosure({ | |
| 30 | + title, | |
| 31 | + children, | |
| 32 | + open, | |
| 33 | + defaultOpen = false, | |
| 34 | + onOpenChange, | |
| 35 | + hidden = false, | |
| 36 | + className, | |
| 37 | +}: FoxDisclosureProps) { | |
| 38 | + const autoId = useId(); | |
| 39 | + const [inner, setInner] = useState(defaultOpen); | |
| 40 | + | |
| 41 | + if (hidden) { | |
| 42 | + return null; | |
| 43 | + } | |
| 44 | + | |
| 45 | + const expanded = open ?? inner; | |
| 46 | + const panelId = `${autoId}-panel`; | |
| 47 | + | |
| 48 | + const toggle = () => { | |
| 49 | + if (open === undefined) { | |
| 50 | + setInner(!expanded); | |
| 51 | + } | |
| 52 | + onOpenChange?.(!expanded); | |
| 53 | + }; | |
| 54 | + | |
| 55 | + return ( | |
| 56 | + <div | |
| 57 | + className={cx( | |
| 58 | + "fox-disclosure", | |
| 59 | + expanded && "fox-disclosure--open", | |
| 60 | + className | |
| 61 | + )} | |
| 62 | + > | |
| 63 | + <button | |
| 64 | + className="fox-disclosure__tit" | |
| 65 | + type="button" | |
| 66 | + aria-expanded={expanded} | |
| 67 | + aria-controls={panelId} | |
| 68 | + onClick={toggle} | |
| 69 | + > | |
| 70 | + <span className="fox-disclosure__icon" aria-hidden="true"> | |
| 71 | + {expanded ? <FoxCaretCircleUpIcon /> : <FoxCaretCircleRightIcon />} | |
| 72 | + </span> | |
| 73 | + <span className="fox-disclosure__title">{title}</span> | |
| 74 | + </button> | |
| 75 | + | |
| 76 | + {/* 닫혔을 때는 DOM에서 뺀다 — 안의 초점 가능한 것들이 탭 순서에 남으면 안 된다. */} | |
| 77 | + {expanded && ( | |
| 78 | + <div className="fox-disclosure__contents" id={panelId}> | |
| 79 | + {children} | |
| 80 | + </div> | |
| 81 | + )} | |
| 82 | + </div> | |
| 83 | + ); | |
| 84 | +} |
+++ @fox/core/components/fox-disclosure/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxDisclosure, type FoxDisclosureProps } from "./fox-disclosure"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -21,6 +21,7 @@ |
| 21 | 21 |
export * from "./fox-chip-area"; |
| 22 | 22 |
export * from "./fox-chip-select-option"; |
| 23 | 23 |
export * from "./fox-conditional-option"; |
| 24 |
+export * from "./fox-disclosure"; |
|
| 24 | 25 |
export * from "./fox-email"; |
| 25 | 26 |
export * from "./fox-file-upload"; |
| 26 | 27 |
export * from "./fox-form-label"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -60,6 +60,7 @@ |
| 60 | 60 |
import { FoxModal } from "../core/components/fox-modal";
|
| 61 | 61 |
import { FoxAccordion } from "../core/components/fox-accordion";
|
| 62 | 62 |
import { FoxCard } from "../core/components/fox-card";
|
| 63 |
+import { FoxDisclosure } from "../core/components/fox-disclosure";
|
|
| 63 | 64 |
import styles from "./dev-test.module.scss"; |
| 64 | 65 |
|
| 65 | 66 |
export interface ComponentExample {
|
... | ... | @@ -3464,6 +3465,35 @@ |
| 3464 | 3465 |
], |
| 3465 | 3466 |
}, |
| 3466 | 3467 |
{
|
| 3468 |
+ id: "fox-disclosure", |
|
| 3469 |
+ name: "FoxDisclosure", |
|
| 3470 |
+ description: |
|
| 3471 |
+ "Figma 시안(disclosure 391:4082 닫힘 · 391:4083 열림)을 옮겼습니다. 캐럿이 제목 앞에 오고 바탕도 테두리도 없어, 글 사이에 끼우는 가벼운 접기입니다 — 목록을 나누는 FoxAccordion과는 다른 컴포넌트입니다. 시안에 크기 축도 계열 축도 없어 변형이 없고 상태는 닫힘·열림 둘뿐입니다(캐럿이 오른쪽 → 위로 바뀝니다). 열림을 밖에서 쥘 수 있고 안 넘기면 스스로 여닫습니다. 닫히면 내용이 DOM에서 빠져 탭 순서에 남지 않습니다.", |
|
| 3472 |
+ variants: [ |
|
| 3473 |
+ {
|
|
| 3474 |
+ label: "시안 그대로 — 닫힘 · 열림", |
|
| 3475 |
+ node: ( |
|
| 3476 |
+ <div className={styles.demoStack}>
|
|
| 3477 |
+ <FoxDisclosure title="디스클로저 레이블" /> |
|
| 3478 |
+ <FoxDisclosure title="디스클로저 레이블" defaultOpen> |
|
| 3479 |
+ <p>펼쳐진 내용이 들어가는 자리입니다.</p> |
|
| 3480 |
+ </FoxDisclosure> |
|
| 3481 |
+ </div> |
|
| 3482 |
+ ), |
|
| 3483 |
+ }, |
|
| 3484 |
+ {
|
|
| 3485 |
+ label: "내용은 슬롯이라 무엇이든 놓입니다", |
|
| 3486 |
+ node: ( |
|
| 3487 |
+ <div className={styles.demoStack}>
|
|
| 3488 |
+ <FoxDisclosure title="첨부된 그림 보기" defaultOpen> |
|
| 3489 |
+ <DemoThumb /> |
|
| 3490 |
+ </FoxDisclosure> |
|
| 3491 |
+ </div> |
|
| 3492 |
+ ), |
|
| 3493 |
+ }, |
|
| 3494 |
+ ], |
|
| 3495 |
+ }, |
|
| 3496 |
+ {
|
|
| 3467 | 3497 |
id: "fox-card", |
| 3468 | 3498 |
name: "FoxCard", |
| 3469 | 3499 |
description: |
+++ @fox/styles/_fox-disclosure.scss
... | ... | @@ -0,0 +1,66 @@ |
| 1 | +// FoxDisclosure — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) | |
| 2 | +// disclosure (391:4082 닫힘 · 391:4083 열림 · 섹션 391:4102) | |
| 3 | +// | |
| 4 | +// 마크업 계약 (React 밖 소비자용): | |
| 5 | +// <div class="fox-disclosure fox-disclosure--open"> | |
| 6 | +// <button class="fox-disclosure__tit" type="button" aria-expanded="true" aria-controls="…"> | |
| 7 | +// <span class="fox-disclosure__icon">…svg…</span> | |
| 8 | +// <span class="fox-disclosure__title">제목</span> | |
| 9 | +// </button> | |
| 10 | +// <div class="fox-disclosure__contents" id="…">…</div> <!-- 열렸을 때만 --> | |
| 11 | +// </div> | |
| 12 | +// | |
| 13 | +// 캐럿이 제목 앞에 오고 바탕·테두리가 없다. 시안에 크기 축도 계열 축도 없어 변형이 없다. | |
| 14 | +// | |
| 15 | +// 토큰이 아닌 값: | |
| 16 | +// - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로 | |
| 17 | +// export하지 않는다. | |
| 18 | +// - 버튼 초기화(appearance/cursor/색 상속): 구조 규칙. | |
| 19 | + | |
| 20 | +@use "abstracts" as fox; | |
| 21 | + | |
| 22 | +.fox-disclosure { | |
| 23 | + display: flex; | |
| 24 | + flex-direction: column; | |
| 25 | + align-items: flex-start; | |
| 26 | + gap: fox.gap(3); | |
| 27 | + inline-size: 100%; | |
| 28 | + | |
| 29 | + // 제목 줄은 통째로 누르는 자리다. 글자 폭만 차지한다 — 시안이 hug다. | |
| 30 | + &__tit { | |
| 31 | + display: flex; | |
| 32 | + align-items: center; | |
| 33 | + gap: fox.gap(2); | |
| 34 | + margin: 0; | |
| 35 | + padding: 0; | |
| 36 | + border: none; | |
| 37 | + background: none; | |
| 38 | + text-align: start; | |
| 39 | + cursor: pointer; | |
| 40 | + } | |
| 41 | + | |
| 42 | + &__icon { | |
| 43 | + display: inline-flex; | |
| 44 | + flex-shrink: 0; | |
| 45 | + inline-size: fox.icon(4); | |
| 46 | + block-size: fox.icon(4); | |
| 47 | + color: fox.color(element-neutral-strong); | |
| 48 | + } | |
| 49 | + | |
| 50 | + &__title { | |
| 51 | + color: fox.color(font-neutral-strong); | |
| 52 | + font-family: fox.font-family(body); | |
| 53 | + font-size: fox.font-size(body-md); | |
| 54 | + font-weight: fox.font-weight(regular); | |
| 55 | + line-height: 1.5; | |
| 56 | + letter-spacing: -0.025em; | |
| 57 | + word-break: break-word; | |
| 58 | + } | |
| 59 | + | |
| 60 | + &__contents { | |
| 61 | + display: flex; | |
| 62 | + flex-direction: column; | |
| 63 | + align-items: flex-start; | |
| 64 | + inline-size: 100%; | |
| 65 | + } | |
| 66 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -58,3 +58,4 @@ |
| 58 | 58 |
@use "fox-list-search"; |
| 59 | 59 |
@use "fox-list-toolbar"; |
| 60 | 60 |
@use "fox-list-container"; |
| 61 |
+@use "fox-disclosure"; |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?