feat: FoxPageHeader 추가
관리자페이지 파일의 page-tit-area(3002:6823)를 옮겼다. 현재 위치와 제목을 얹고 본문과의 간격(아래 여백 36px)까지 갖는다 — 시안이 그렇게 묶어 놓아 화면마다 따로 띄우지 않아도 된다. 현재 위치는 FoxBreadcrumb를 그대로 쓴다(시안에서도 breadcrumb 인스턴스다). 항목 배열만 넘기면 되고, 넘기지 않으면 그 줄을 그리지 않는다. 제목은 화면에 하나뿐인 이름이라 ``으로 낸다. 시안은 `
`지만 그건 Figma에 문단밖에 없어서고, 마크업에서는 제목이 맞다. `actions` 자리를 열어 뒀다. 시안에 들어간 것은 없지만 배치가 자리를 비워 두고 있다 — 제목 칸이 `flex: 1`이고 오른쪽에 24px 간격이 남는다. 값을 지어내지 않고 그 자리만 연다. 검증: 바깥 간격 24 · 아래 여백 36 · 안쪽 간격 8 · 제목 32px w800 #1e2124 줄높이 48 · 자간 -0.8px이 시안과 일치. 오른쪽 자리를 쓸 때 제목이 남는 폭을 먹고 24px이 유지되며, 제목이 길어져도 오른쪽을 침범하지 않는다. sass 634규칙 · tsc · eslint 통과. Co-Authored-By: Claude Opus 5
@21f76593ae723dcb92d582ca2425206521706ff8
+++ @fox/core/components/fox-page-header/fox-page-header.tsx
... | ... | @@ -0,0 +1,51 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import type { ReactNode } from "react"; | |
| 4 | +import { cx } from "../../utils"; | |
| 5 | +import { FoxBreadcrumb, type FoxBreadcrumbItem } from "../fox-breadcrumb"; | |
| 6 | + | |
| 7 | +export interface FoxPageHeaderProps { | |
| 8 | + /** 화면 제목. 페이지에 하나뿐인 이름이라 `<h1>`으로 나간다. */ | |
| 9 | + title: ReactNode; | |
| 10 | + /** 제목 위에 붙는 현재 위치. 넘기지 않으면 영역을 그리지 않는다. */ | |
| 11 | + breadcrumb?: FoxBreadcrumbItem[]; | |
| 12 | + /** | |
| 13 | + * 제목 오른쪽 자리. 시안에는 들어간 것이 없지만 배치가 자리를 비워 두고 있어 | |
| 14 | + * (제목이 남는 폭을 먹고 24px 간격이 있다) 화면이 버튼 같은 것을 얹을 수 있게 연다. | |
| 15 | + */ | |
| 16 | + actions?: ReactNode; | |
| 17 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 18 | + hidden?: boolean; | |
| 19 | + /** 배치 조정용. */ | |
| 20 | + className?: string; | |
| 21 | +} | |
| 22 | + | |
| 23 | +/** | |
| 24 | + * @fox 화면 머리말. 현재 위치와 제목을 얹고 본문과의 간격까지 갖는다. | |
| 25 | + * | |
| 26 | + * 현재 위치는 `FoxBreadcrumb`를 그대로 쓴다 — 시안에서도 breadcrumb 인스턴스다. | |
| 27 | + * | |
| 28 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 29 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 30 | + */ | |
| 31 | +export function FoxPageHeader({ | |
| 32 | + title, | |
| 33 | + breadcrumb, | |
| 34 | + actions, | |
| 35 | + hidden = false, | |
| 36 | + className, | |
| 37 | +}: FoxPageHeaderProps) { | |
| 38 | + if (hidden) { | |
| 39 | + return null; | |
| 40 | + } | |
| 41 | + | |
| 42 | + return ( | |
| 43 | + <div className={cx("fox-page-header", className)}> | |
| 44 | + <div className="fox-page-header__main"> | |
| 45 | + {breadcrumb && breadcrumb.length > 0 && <FoxBreadcrumb items={breadcrumb} />} | |
| 46 | + <h1 className="fox-page-header__title">{title}</h1> | |
| 47 | + </div> | |
| 48 | + {actions && <div className="fox-page-header__actions">{actions}</div>} | |
| 49 | + </div> | |
| 50 | + ); | |
| 51 | +} |
+++ @fox/core/components/fox-page-header/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxPageHeader, type FoxPageHeaderProps } from "./fox-page-header"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -25,6 +25,7 @@ |
| 25 | 25 |
export * from "./fox-icon-button"; |
| 26 | 26 |
export * from "./fox-input"; |
| 27 | 27 |
export * from "./fox-link-button"; |
| 28 |
+export * from "./fox-page-header"; |
|
| 28 | 29 |
export * from "./fox-pagination"; |
| 29 | 30 |
export * from "./fox-phone-number"; |
| 30 | 31 |
export * from "./fox-radio"; |
... | ... | @@ -48,4 +49,3 @@ |
| 48 | 49 |
export * from "./fox-text-button"; |
| 49 | 50 |
export * from "./fox-toggle-quantity"; |
| 50 | 51 |
export * from "./fox-toggle-switch"; |
| 51 |
- |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -56,6 +56,7 @@ |
| 56 | 56 |
import { FoxSegmentedControl } from "../core/components/fox-segmented-control";
|
| 57 | 57 |
import { FoxSegmentedControlItem } from "../core/components/fox-segmented-control-item";
|
| 58 | 58 |
import { FoxToggleQuantity } from "../core/components/fox-toggle-quantity";
|
| 59 |
+import { FoxPageHeader } from "../core/components/fox-page-header";
|
|
| 59 | 60 |
import styles from "./dev-test.module.scss"; |
| 60 | 61 |
|
| 61 | 62 |
export interface ComponentExample {
|
... | ... | @@ -3255,4 +3256,45 @@ |
| 3255 | 3256 |
}, |
| 3256 | 3257 |
], |
| 3257 | 3258 |
}, |
| 3259 |
+ {
|
|
| 3260 |
+ id: "fox-page-header", |
|
| 3261 |
+ name: "FoxPageHeader", |
|
| 3262 |
+ description: |
|
| 3263 |
+ "Figma 시안(page-tit-area 3002:6823, 관리자페이지 파일)을 옮겼습니다. 현재 위치와 제목을 얹고 본문과의 간격(아래 여백)까지 갖습니다 — 시안이 그렇게 묶어 놓아 화면마다 따로 띄우지 않아도 됩니다. 제목은 화면에 하나뿐인 이름이라 <h1>으로 나갑니다.", |
|
| 3264 |
+ variants: [ |
|
| 3265 |
+ {
|
|
| 3266 |
+ label: "시안 그대로 — 현재 위치 + 제목", |
|
| 3267 |
+ node: <FoxPageHeader breadcrumb={BREADCRUMB_ITEMS} title="리스트 페이지" />,
|
|
| 3268 |
+ }, |
|
| 3269 |
+ {
|
|
| 3270 |
+ label: "현재 위치 없이 제목만", |
|
| 3271 |
+ node: <FoxPageHeader title="리스트 페이지" />, |
|
| 3272 |
+ }, |
|
| 3273 |
+ {
|
|
| 3274 |
+ label: "오른쪽 자리 (actions) — 시안엔 없지만 배치가 자리를 비워 둡니다", |
|
| 3275 |
+ node: ( |
|
| 3276 |
+ <FoxPageHeader |
|
| 3277 |
+ breadcrumb={BREADCRUMB_ITEMS}
|
|
| 3278 |
+ title="리스트 페이지" |
|
| 3279 |
+ actions={
|
|
| 3280 |
+ <FoxButtonGroup size="md"> |
|
| 3281 |
+ <FoxButton size="md" type="secondary" label="엑셀 다운로드" /> |
|
| 3282 |
+ <FoxButton size="md" type="primary" label="등록" /> |
|
| 3283 |
+ </FoxButtonGroup> |
|
| 3284 |
+ } |
|
| 3285 |
+ /> |
|
| 3286 |
+ ), |
|
| 3287 |
+ }, |
|
| 3288 |
+ {
|
|
| 3289 |
+ label: "제목이 길어질 때 — 오른쪽 자리를 침범하지 않고 접힙니다", |
|
| 3290 |
+ node: ( |
|
| 3291 |
+ <FoxPageHeader |
|
| 3292 |
+ breadcrumb={BREADCRUMB_ITEMS}
|
|
| 3293 |
+ title="아주 긴 화면 제목이 들어가면 이렇게 여러 줄로 접힙니다 아주 긴 화면 제목" |
|
| 3294 |
+ actions={<FoxButton size="md" type="primary" label="등록" />}
|
|
| 3295 |
+ /> |
|
| 3296 |
+ ), |
|
| 3297 |
+ }, |
|
| 3298 |
+ ], |
|
| 3299 |
+ }, |
|
| 3258 | 3300 |
]; |
+++ @fox/styles/_fox-page-header.scss
... | ... | @@ -0,0 +1,60 @@ |
| 1 | +// FoxPageHeader — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) page-tit-area (3002:6823) | |
| 2 | +// | |
| 3 | +// 마크업 계약 (React 밖 소비자용): | |
| 4 | +// <div class="fox-page-header"> | |
| 5 | +// <div class="fox-page-header__main"> | |
| 6 | +// …fox-breadcrumb… <!-- 선택 --> | |
| 7 | +// <h1 class="fox-page-header__title">제목</h1> | |
| 8 | +// </div> | |
| 9 | +// <div class="fox-page-header__actions">…</div> <!-- 선택 --> | |
| 10 | +// </div> | |
| 11 | +// | |
| 12 | +// 제목이 남는 폭을 먹고 오른쪽에 24px 간격이 남는다. 시안에는 그 자리에 들어간 것이 없지만 | |
| 13 | +// 배치가 자리를 비워 두고 있어 `__actions`로 열어 둔다. | |
| 14 | +// | |
| 15 | +// 본문과의 간격(아래 여백)을 머리말이 갖는다 — 시안이 그렇게 묶어 놓았다. 화면마다 따로 | |
| 16 | +// 띄우지 않아도 된다. | |
| 17 | +// | |
| 18 | +// 토큰이 아닌 값: | |
| 19 | +// - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로 | |
| 20 | +// export하지 않는다. | |
| 21 | +// - `<h1>`의 브라우저 기본 여백 0: UA 기본값 지우기. | |
| 22 | + | |
| 23 | +@use "abstracts" as fox; | |
| 24 | + | |
| 25 | +.fox-page-header { | |
| 26 | + display: flex; | |
| 27 | + align-items: flex-start; | |
| 28 | + gap: fox.gap(7); | |
| 29 | + inline-size: 100%; | |
| 30 | + padding-block-end: fox.spacing(bottom-xlg); | |
| 31 | + | |
| 32 | + // 현재 위치와 제목이 세로로 쌓이고 남는 폭을 먹는다. | |
| 33 | + &__main { | |
| 34 | + display: flex; | |
| 35 | + flex: 1 0 0; | |
| 36 | + flex-direction: column; | |
| 37 | + align-items: flex-start; | |
| 38 | + gap: fox.gap(3); | |
| 39 | + min-inline-size: 0; | |
| 40 | + } | |
| 41 | + | |
| 42 | + &__title { | |
| 43 | + inline-size: 100%; | |
| 44 | + margin: 0; | |
| 45 | + color: fox.color(font-neutral-strong); | |
| 46 | + font-family: fox.font-family(title); | |
| 47 | + font-size: fox.font-size(display-md); | |
| 48 | + font-weight: fox.font-weight(bold); | |
| 49 | + line-height: 1.5; | |
| 50 | + letter-spacing: -0.025em; | |
| 51 | + word-break: break-word; | |
| 52 | + } | |
| 53 | + | |
| 54 | + // 제목 옆에 서고 줄어들지 않는다. | |
| 55 | + &__actions { | |
| 56 | + display: flex; | |
| 57 | + flex-shrink: 0; | |
| 58 | + align-items: center; | |
| 59 | + } | |
| 60 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -51,3 +51,4 @@ |
| 51 | 51 |
@use "fox-tab-item"; |
| 52 | 52 |
@use "fox-spinner"; |
| 53 | 53 |
@use "fox-toggle-quantity"; |
| 54 |
+@use "fox-page-header"; |
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?