임동욱 임동욱 08-12
feat: FoxButtonPanel 추가
Figma btn-page(404:5121)의 네 변형을 옮긴다. 자식은 버튼 그룹이고 여럿이면
양끝, 하나면 가운데다.

자식 수를 React에서 세지 않고 CSS `:has(> :only-child)`로 판단한다. 프래그먼트나
조건부 렌더로 자식이 갈려도 실제 DOM이 기준이라 어긋나지 않고, React를 안 쓰는
소비자도 표시할 클래스가 없다.

패널이 자식에게 강제하는 두 가지(사용자 승인):
  - 패널 안 그룹 간격은 gap-3(8px). 단독으로 쓰는 그룹의 gap-2(4px)는 그대로다.
  - 모바일에서는 패널도 그룹도 세로로 쌓이고 버튼이 폭을 채운다.

시안이 패널 안 버튼에만 주는 min-width 160px은 변수 바인딩이 없고 토큰 스케일에도
160이 없어 리터럴로 둔다(사용자 승인). 상단 80px 여백은 spacing-top-2xlg이며
offset={false}로 끌 수 있다.

Co-Authored-By: Claude Opus 5 
@e440f2a8d2ffebf15f4e565db33c28a7b56be30a
@fox/README.md
--- @fox/README.md
+++ @fox/README.md
@@ -95,6 +95,11 @@
   <button class="fox-button fox-button--default fox-button--md">취소</button>
   <button class="fox-button fox-button--primary fox-button--md">확인</button>
 </div>
+
+<div class="fox-button-panel fox-button-panel--offset">
+  <div class="fox-button-group fox-button-group--horizontal fox-button-group--start">…</div>
+  <div class="fox-button-group fox-button-group--horizontal fox-button-group--end">…</div>
+</div>
 ```
 
 - 비활성은 네이티브 `disabled` 속성으로 표현합니다(별도 클래스 없음). 앵커에는 `disabled`가
@@ -104,6 +109,9 @@
 - 버튼 묶음은 간격·방향·정렬만 담당합니다. 자식 버튼의 크기 클래스를 그룹 크기로 맞추는 건
   작성자 몫입니다(React 래퍼는 `size` prop으로 자동화합니다). 세로(`--vertical`)는 자식이
   폭을 채웁니다.
+- 버튼 패널은 그룹이 여럿이면 양끝으로, 하나면 가운데로 둡니다 — 자식 수는 CSS `:only-child`가
+  판단하므로 따로 표시할 게 없습니다. 패널 안에서는 그룹 간격이 넓어지고 버튼에 최소 폭이
+  붙으며, 모바일에서는 패널·그룹이 모두 세로로 쌓입니다.
 - 아이콘 SVG는 `currentColor`로 그려야 계열별 색이 적용됩니다. 크기는 슬롯이 정합니다.
 
 `core/`의 React 컴포넌트는 이 클래스를 조립해주는 **얇은 래퍼**일 뿐입니다 — 쓰지 않아도
 
@fox/core/components/fox-button-panel/fox-button-panel.tsx (added)
+++ @fox/core/components/fox-button-panel/fox-button-panel.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { cx } from "../../utils";
+
+export interface FoxButtonPanelProps {
+  /** 시안의 상단 80px 여백. 페이지 하단이 아닌 곳에 놓을 땐 끈다. */
+  offset?: boolean;
+  /** 배치 조정용. */
+  className?: string;
+  /** 버튼 그룹들. 하나면 가운데, 여럿이면 양끝으로 벌어진다. */
+  children?: ReactNode;
+}
+
+/**
+ * @fox 페이지 하단 동작 영역. 배치만 책임지고 상태를 갖지 않는다.
+ *
+ * 자식 수를 세지 않는다 — 가운데 정렬은 CSS `:only-child`가 판단하므로 프래그먼트나
+ * 조건부 렌더로 자식이 갈려도 실제 DOM 기준으로 맞는다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxButtonPanel({
+  offset = true,
+  className,
+  children,
+}: FoxButtonPanelProps) {
+  return (
+    <div
+      className={cx("fox-button-panel", offset && "fox-button-panel--offset", className)}
+    >
+      {children}
+    </div>
+  );
+}
 
@fox/core/components/fox-button-panel/index.ts (added)
+++ @fox/core/components/fox-button-panel/index.ts
@@ -0,0 +1,1 @@
+export { FoxButtonPanel, type FoxButtonPanelProps } from "./fox-button-panel";
@fox/core/components/index.ts
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
@@ -3,3 +3,4 @@
 export * from "./fox-link-button";
 export * from "./fox-icon-button";
 export * from "./fox-button-group";
+export * from "./fox-button-panel";
@fox/dev-test/component-registry.tsx
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
@@ -6,6 +6,7 @@
 import { FoxLinkButton } from "../core/components/fox-link-button";
 import { FoxIconButton } from "../core/components/fox-icon-button";
 import { FoxButtonGroup } from "../core/components/fox-button-group";
+import { FoxButtonPanel } from "../core/components/fox-button-panel";
 import styles from "./dev-test.module.scss";
 
 export interface ComponentExample {
@@ -445,4 +446,71 @@
       },
     ],
   },
+  {
+    id: "fox-button-panel",
+    name: "FoxButtonPanel",
+    description:
+      "Figma 시안(btn-page, 컴포넌트 세트 404:5121)을 옮겼습니다. 자식은 버튼 그룹이고, 여럿이면 양끝으로 벌어지고 하나면 가운데로 갑니다 — 자식 수는 CSS :only-child가 판단합니다. 패널 안에서는 그룹 간격이 gap-3(8px)으로 덮어써지고(단독으로 쓰면 gap-2), 버튼에 최소 폭 160px이 붙습니다. 창을 768px 아래로 줄이면 패널도 그룹도 세로로 쌓이고 버튼이 폭을 채웁니다.",
+    variants: [
+      {
+        label: "그룹 둘 — 양끝 (시안 type=flow)",
+        node: (
+          <div className={styles.demoOutline}>
+            <FoxButtonPanel offset={false}>
+              <FoxButtonGroup size="lg">
+                <FoxButton size="lg" label="취소" />
+              </FoxButtonGroup>
+              <FoxButtonGroup size="lg">
+                <FoxButton size="lg" type="secondary" label="임시 저장" />
+                <FoxButton size="lg" type="primary" label="다음" />
+              </FoxButtonGroup>
+            </FoxButtonPanel>
+          </div>
+        ),
+      },
+      {
+        label: "그룹 하나 — 가운데 (시안 type=default)",
+        node: (
+          <div className={styles.demoOutline}>
+            <FoxButtonPanel offset={false}>
+              <FoxButtonGroup size="lg">
+                <FoxButton size="lg" label="취소" />
+                <FoxButton size="lg" type="primary" label="확인" />
+              </FoxButtonGroup>
+            </FoxButtonPanel>
+          </div>
+        ),
+      },
+      {
+        label: "상단 여백 (offset) — 기본 켜짐, 80px",
+        node: (
+          <div className={styles.demoOutline}>
+            <FoxButtonPanel>
+              <FoxButtonGroup size="lg">
+                <FoxButton size="lg" type="primary" label="확인" />
+              </FoxButtonGroup>
+            </FoxButtonPanel>
+          </div>
+        ),
+      },
+      {
+        label: "그룹 셋 이상도 양끝 기준으로 벌어진다",
+        node: (
+          <div className={styles.demoOutline}>
+            <FoxButtonPanel offset={false}>
+              <FoxButtonGroup size="lg">
+                <FoxButton size="lg" label="삭제" />
+              </FoxButtonGroup>
+              <FoxButtonGroup size="lg">
+                <FoxButton size="lg" label="미리보기" />
+              </FoxButtonGroup>
+              <FoxButtonGroup size="lg">
+                <FoxButton size="lg" type="primary" label="저장" />
+              </FoxButtonGroup>
+            </FoxButtonPanel>
+          </div>
+        ),
+      },
+    ],
+  },
 ];
 
@fox/styles/_fox-button-panel.scss (added)
+++ @fox/styles/_fox-button-panel.scss
@@ -0,0 +1,63 @@
+// FoxButtonPanel — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                        Figma btn-page (컴포넌트 세트 404:5121 · 섹션 405:5193)
+//
+// 페이지 하단 동작 영역. 자식은 버튼 그룹이고, 여럿이면 양끝으로 벌리고 하나면 가운데 둔다.
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-button-panel fox-button-panel--offset">
+//     <div class="fox-button-group fox-button-group--horizontal fox-button-group--start">…</div>
+//     <div class="fox-button-group fox-button-group--horizontal fox-button-group--end">…</div>
+//   </div>
+// 자식이 하나면 `:only-child`가 알아서 가운데로 보낸다 — 별도 클래스가 필요 없고, 프래그먼트나
+// 조건부 렌더로 자식 수가 갈리는 React 쪽 계산 실수도 생기지 않는다(DOM이 기준이다).
+//
+// 시안이 요구하는 두 가지를 패널이 자식에게 강제한다(사용자 승인):
+//   - 패널 안 그룹의 간격은 `gap/3`이다. 그룹 단독으로 쓸 때의 `gap/2`를 덮어쓴다.
+//   - 모바일에서는 패널도 그룹도 세로로 쌓이고 버튼이 폭을 채운다.
+//
+// 토큰이 아닌 값(사용자 승인):
+//   - 버튼 `min-inline-size: 16rem`(160px): 시안이 패널 안 버튼에만 주는 값인데 변수로
+//     바인딩되어 있지 않고 토큰 스케일에도 160이 없다. `.fox-button`에만, 패널 안에서만 건다.
+//   - box-sizing: 상단 여백을 패딩으로 주므로 필요하다.
+
+@use "abstracts" as fox;
+
+.fox-button-panel {
+  box-sizing: border-box;
+  display: flex;
+  inline-size: 100%;
+  gap: fox.gap(3);
+  align-items: flex-start;
+  justify-content: space-between;
+
+  // 시안의 80px `margin` 프레임. 다른 맥락에 놓을 땐 끈다.
+  &--offset {
+    padding-block-start: fox.spacing(top-2xlg);
+  }
+
+  &:has(> :only-child) {
+    justify-content: center;
+  }
+
+  .fox-button-group {
+    gap: fox.gap(3);
+  }
+
+  .fox-button {
+    min-inline-size: 16rem;
+  }
+
+  @include fox.mobile {
+    flex-direction: column;
+    align-items: stretch;
+
+    .fox-button-group {
+      flex-direction: column;
+      align-items: stretch;
+    }
+
+    .fox-button {
+      inline-size: 100%;
+    }
+  }
+}
@fox/styles/components.scss
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
@@ -6,3 +6,4 @@
 @use "fox-link-button";
 @use "fox-icon-button";
 @use "fox-button-group";
+@use "fox-button-panel";
Add a comment
List