feat: integrate latest HMDesign design system updates

This commit is contained in:
DESKTOP-KPCP6AL\ㅅㅎ committed 2026-10-11 11:09:03 +09:00
1 parent 99faec189e
commit 1c4e0bb9a1
209 files changed
+8845 -552

No files matched your search

+4
View File
@@ -59,6 +59,8 @@ Dashboard 운영 화면은 전용 Header/Menu·업무 상태 필터와 Content
상태 CSS는 변수 정의만 제공한다. 기존 `Badge variant="outline"`에는 화면의 wrapper에서 아래 mapping을 적용한다. 완료의 흰색 ink가 유지되므로 기존 어두운 Badge 배경도 함께 유지한다. `issue` Badge는 실제 상태 Badge와 별도로 렌더링한다.
이 recipe와 일치하더라도 소비자의 HM slot CSS는 Guard `REUSE-003 NOT VERIFIED`를 유지한다. [CSS Intent 근거 연결](../guidelines/ai-generation-guard.md#css-측정과-intent-검토-근거)은 정확한 issue·문서 SHA·실측 변수를 연결하며 스타일 적용 확인과 사용자 디자인 의도 승인을 구분한다.
```css
.dashboard-status [data-status="active"] { --dashboard-status-color: var(--hm-dashboard-status-active); }
.dashboard-status [data-status="hold"] { --dashboard-status-color: var(--hm-dashboard-status-hold); }
@@ -73,6 +75,8 @@ Dashboard 운영 화면은 전용 Header/Menu·업무 상태 필터와 Content
HTML adapter는 같은 상태 변수를 자신의 `color` 등에 연결한다. package dist를 정적 자산으로 배치하면 `theme.css` 다음에 `dashboard-status.css`를 `<link>`로 로드한다. 저장소의 기존 Static HTML 예제는 호환 CSS 경로를 그대로 사용한다. React와 HTML 모두 `done`은 `--hm-dashboard-status-done`을 참조하며, 이 alias는 `:root`와 `[data-hm-theme]`에서 각 scope의 white token으로 재계산한다. active/hold/issue 색상과 Runtime Theme의 73개 계약은 유지한다.
Guard의 REUSE-003는 위 소비 mapping에도 검토 위치를 남긴다. Token 선언과 실제 Badge custom property/color/background/border를 통과해도 요청 디자인과의 의미적 일치는 자동 승인하지 않는다. [CSS Intent·동적 Token 증거 계약](../guidelines/ai-generation-guard.md#css-intent와-동적-token-검사-2026-10-10)에 따라 위치별 STATIC/RUNTIME/DESIGN_INTENT/FINAL을 보고하고, 실제 실행하지 않은 Theme와 상태는 NOT VERIFIED로 남긴다. 기존 대표 Story·상태 색상·Layout은 그대로 유지한다.
#### React 소비 예제: 데이터 → 상태 변수 → Badge
`active`(진행) → `--hm-dashboard-status-active`, `hold`(보류) → `--hm-dashboard-status-hold`, `done`(완료) → `--hm-dashboard-status-done`이다. 독립 `issue === true`는 `--hm-dashboard-status-issue`로 별도 Badge를 표시한다. `tone`은 의미 분류이며 Badge variant나 CSS를 자동 선택하지 않는다. 위 CSS mapping과 import 순서를 앱 CSS entry에 적용한 다음 아래 공개 API를 사용한다.
+19 -1
View File
@@ -1,5 +1,7 @@
# Data Table (Pattern)
> 2026-10-10 추가 수정 요청 기준: 기본 검색 가운데 / 추가 조건 검색 왼쪽, 조건→검색어→조회→초기화·8px. Header는 Card 밖, 검색·목록 도구·결과는 한 Card. 페이지 크기 label useId로 인스턴스 분리. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7, Pattern 절)를 따른다. 이 문서는 Data Table Pattern 고유의 API·상태·결정만 기록한다.
## 1. 상태
@@ -60,7 +62,7 @@ Table, Checkbox(header mixed), Pagination, Select(페이지 크기), Dropdown Me
## 8. Styling / Token
Header·cell·row·selected 면은 `packages/ui/src/theme/table.css`(`--hm-table-*`)가 소유하고 Pattern은 다시 정의하지 않는다. 컬러 역할(2026-10-06): 선택 행 면 = `--accent` 45%(hover·selected+hover와 같은 Soft Surface, Select Highlight / Selected와 동일한 Accent 45% 정책), 선택 Checkbox = `--hm-sem-state-active`(Light 2). 면은 hover와 같으므로 선택 여부는 Checkbox와 선택 수 요약으로 구분한다. Pattern 전용: `packages/ui/src/theme/data-table.css`의 `--hm-data-table-*`(footer gap·padding, summary 색·크기, 정렬 버튼 간격·아이콘 크기·활성 색, empty padding·색, skeleton 높이). 신규 Primitive·Semantic 없음.
Header·cell·row·selected 면은 `packages/ui/src/theme/table.css`(`--hm-table-*`)가 소유하고 Pattern은 다시 정의하지 않는다. 컬러 역할(2026-10-06): 선택 행 면 = `--hm-sem-accent` 20%(hover·selected+hover와 같은 Soft Surface, Table에만 적용하는 20% 정책; Select 강조는 기존 45% 유지), 선택 Checkbox = `--hm-sem-state-active`(Light 2). 면은 hover와 같으므로 선택 여부는 Checkbox와 선택 수 요약으로 구분한다. Pattern 전용: `packages/ui/src/theme/data-table.css`의 `--hm-data-table-*`(footer gap·padding, summary 색·크기, 정렬 버튼 간격·아이콘 크기·활성 색, empty padding·색, skeleton 높이). 신규 Primitive·Semantic 없음.
- Loading: header 유지, body는 Skeleton 행(최대 5행), `aria-busy`.
- Sort Direction Accent: `--hm-data-table-sort-active-color`는 현재 `--hm-sem-palette-green-light-3`(Green Light 3 palette)를 참조한다. 별도 State Semantic으로 승격하지 않았으며 Primary/Active/Accent 역할에 자동 연결되지 않는다. Token 이름의 `active`는 Control Active Semantic 계약을 뜻하지 않는다.
@@ -79,6 +81,22 @@ semantic `<table>` 유지. 정렬 열 `<th aria-sort>`(ascending / descending),
Table primitive의 container가 가로 스크롤한다. Pattern은 `[data-slot="data-table"] { min-width: 0 }`로 grid / flex 부모 안에서 페이지를 밀지 않게 한다. 390px에서 페이지 가로 overflow 없음(표만 520px 안에서 스크롤), footer는 요약 / 페이지 크기 / 페이지 이동이 줄바꿈된다. 카드형 변환은 없다.
### 11.1 식별자 줄바꿈과 열 폭
Table의 `overflow-wrap: anywhere`는 긴 일반 콘텐츠를 보호하지만 좁은 ID 열에서 `JOB-001`을 여러 줄로 분절할 수 있다. ID·날짜 등 의미상 한 줄이어야 하는 값은 해당 cell의 콘텐츠만 보호한다. 제목 등 장문은 기존 줄바꿈을 유지한다.
```tsx
const columns: DataTableColumnDef<Job>[] = [
{ accessorKey: 'id', header: 'ID', enableSorting: false,
cell: ({ row }) => <span className="whitespace-nowrap">{row.original.id}</span> },
// 제목·그 외 열은 요구사항에 맞는 기존 cell composition을 유지한다.
]
```
현재 DataTable은 `ColumnDef.size/minSize`를 DOM의 th/td width 또는 min-width로 연결하지 않는다. 해당 값만 지정하고 폭이 확보됐다고 보고하지 않는다. `nowrap` 콘텐츠의 실제 폭·cell padding·표 내부 스크롤을 측정한다. 긴 식별자의 별도 최소 폭이 필요하면 소비 콘텐츠의 min-width composition과 실제 DOM 폭을 확인한다. Typography·Color·공통 Table CSS는 변경하지 않는다. Ellipsis를 쓰는 요구사항이라면 키보드/터치로 전체 값을 확인할 경로를 함께 제공하며 ID를 잘라서 식별 가능하다고 판단하지 않는다.
네 폭에서 ID 전체 텍스트·Range 줄 수·cell 내용 폭·table/container scrollWidth/clientWidth와 문서 가로 넘침을 기록한다. 긴 제목 줄바꿈, 표 내부 가로 스크롤과 마지막 열 접근을 함께 확인한다. `min-width: 0`만으로 ID 가독성을 보장하지 않는다.
## 12. 보류 / 비범위
- column resizing·pinning, grouping, tree, virtualization, infinite scroll, drag reorder, 서버 사이드 페이지/정렬, 편집 가능한 표.
+20 -13
View File
@@ -1,5 +1,7 @@
# Filter Bar (Pattern)
> 2026-10-10 추가 수정 요청 기준: 공통 FilterBar 기본 배치는 유지. 목록 소비자는 공개 `layout="list-basic" | "list-extended"` prop으로 가운데·왼쪽 정렬과 actions auto margin 제거를 선택한다. 기존 [src/list-screen.css](../../src/list-screen.css) 소비자는 호환성을 유지한다. PageHeader 밖의 흰 Card 하나에 조건→검색어→조회→초기화 순서, 간격 8px, 조회 Solid Green·초기화 Outline Gray를 적용한다. readOnly·disabled Search에는 Clear 버튼을 노출하지 않는다. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7, Pattern 절)를 따른다.
## 1. 상태
@@ -14,7 +16,7 @@
## 2. 목적 / 사용 범위
목록·업무 화면의 검색 조건 행. Data Table 없이도 쓸 수 있고, 값은 소비자가 소유한다. ui-viz의 Search Field(선택값 칩 + 후보)는 별개의 복합 컨트롤이라 이 Pattern에 넣지 않았다.
목록·업무 화면의 검색 조건 행. Table·Card 목록·모바일 목록의 검색에서 우선 검토하며 Data Table 없이도 쓸 수 있고, 값은 소비자가 소유한다. 맞는 부분만 조합할 수 있다. 요구사항이 chips/복수 검색 등 공개 API·구조로 표현되지 않으면 구체적인 미사용 이유를 기록하고 HM Component로 조합한다. ui-viz의 Search Field(선택값 칩 + 후보)는 별개의 복합 컨트롤이라 이 Pattern에 넣지 않았다.
## 3. 구성 요소
@@ -23,16 +25,19 @@
## 4. 구조
```tsx
<FilterBar>
<FilterBarSearch value={q} onValueChange={setQ} placeholder="이름·부서 검색" />
<FilterBar layout="list-extended">
<Select>…</Select> <Combobox … /> <DatePicker … />
<FilterBarActions><Button variant="cancel" appearance="outline">초기화</Button></FilterBarActions>
<FilterBarSearch value={q} onValueChange={setQ} placeholder="이름·부서 검색" />
<FilterBarActions>
<Button type="button" variant="primary" appearance="solid" onClick={apply}>조회</Button>
<Button type="button" variant="cancel" appearance="outline" onClick={reset}>초기화</Button>
</FilterBarActions>
</FilterBar>
```
## 5. Public API
- `FilterBar`, `FilterBarActions`: `div` props. wrapper에 role을 두지 않는다.
- `FilterBar`: `div` props + 선택적 `layout: "default" | "list-basic" | "list-extended"`. 기본은 기존 배치 유지. `FilterBarProps` 공개 type 추가. `FilterBarActions`: `div` props. wrapper에 role을 두지 않는다.
- `FilterBarSearch`: `value`, `onValueChange`(필수), `size`(Input size, 기본 `medium`), `placeholder`(기본 "검색"), 나머지 `input` props. `aria-label` 기본값은 placeholder. 값이 있으면 지우기 버튼(`aria-label="검색어 지우기"`).
- Reset은 소비자가 Button과 callback으로 구성한다(Pattern에 `onReset` prop 없음).
- Dashboard의 빠른 상태 전환(`all / active / hold / done / issue`)은 `FilterBar`가 소유하지 않는다. `FilterBar`는 검색·Select·Combobox·DatePicker 등의 조회 조건 layout으로 유지하고, 상태 필터는 Dashboard status contract를 소비하는 별도 조합으로 배치한다.
@@ -41,9 +46,9 @@
Pattern은 상태가 없다. Data Table과 연결할 때는 검색 → `table.setGlobalFilter`, 열 선택 → `column.setFilterValue`로 소비자가 잇는다(`data-table.md` §6).
FilterBar에는 `onSearch / onSubmit / onReset` 공개 API나 조회 시점 강제가 없다. 검색어 지우기도 `onValueChange("")`만 호출한다. 즉시 적용할지 조회 버튼으로 적용할지, 초기화 후 바로 전체 목록을 복원할지는 소비자가 결정한다. [대표 Story](../../src/patterns/filter-bar.stories.tsx)의 조회 버튼에는 실행 callback이 없고 조건 수는 입력값으로 계산한다. [DataTable 연동 verification](../../src/patterns/data-table.stories.tsx)의 `WithFilterBarStory`는 입력 즉시 table 필터를 변경하며 조회 버튼은 없다. 이 둘을 조회 버튼 적용 계약으로 해석하지 않는다.
FilterBar에는 `onSearch / onSubmit / onReset` 공개 API나 조회 시점 강제가 없다. 검색어 지우기도 `onValueChange("")`만 호출한다. 즉시 적용할지 조회 버튼으로 적용할지, 초기화 후 바로 전체 목록을 복원할지는 소비자가 결정한다. [대표 Story](../../src/patterns/filter-bar.stories.tsx)는 입력 조건 수와 조회 적용 조건 수를 구분하고 조회/초기화 callback을 실행한다. [DataTable 연동 verification](../../src/patterns/data-table.stories.tsx)의 `WithFilterBarStory`는 입력 즉시 table 필터를 변경하며 조회 버튼은 없다. 이 둘을 조회 버튼 적용 계약으로 해석하지 않는다.
DatePicker / Combobox는 대표 Story에서 `controlled={true}`로 사용한다. 부모가 각 `value`를 소유하고 초기화 시 `setDate(undefined)` / `setCategory(undefined)`로 비우므로, 선택값 초기화 목적의 `key` remount는 필요 없다. 검색·상태 등 나머지 조회 조건도 부모가 함께 초기화한다. FilterBar 공개 API와 초기화 / 조회 Outline 버튼 세트는 유지한다. 외부 값 초기화는 자식 `onValueChange`를 호출하지 않으며, 팝업 닫기·검색어·달력 월 초기화를 보장하지 않는다.
DatePicker / Combobox는 대표 Story에서 `controlled={true}`로 사용한다. 부모가 각 `value`를 소유하고 초기화 시 `setDate(undefined)` / `setCategory(undefined)`로 비우므로, 선택값 초기화 목적의 `key` remount는 필요 없다. 검색·상태 등 나머지 조회 조건도 부모가 함께 초기화한다. FilterBar 공개 API는 유지하고 목록의 조회/초기화 버튼은 위 최신 소비 계약을 따른다. 외부 값 초기화는 자식 `onValueChange`를 호출하지 않으며, 팝업 닫기·검색어·달력 월 초기화를 보장하지 않는다.
### 6.1 조회 버튼으로 적용하는 소비 예제
@@ -52,7 +57,7 @@ DatePicker / Combobox는 대표 Story에서 `controlled={true}`로 사용한다.
```tsx
import { useMemo, useState } from "react"
import {
Button, DataTable, FilterBar, FilterBarActions, FilterBarSearch,
Button, Card, CardContent, DataTable, FilterBar, FilterBarActions, FilterBarSearch,
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useDataTable,
type DataTableColumnDef,
} from "@hm-design/ui"
@@ -84,8 +89,8 @@ export function QueryList() {
table.setPageIndex(0)
}
return <>
<FilterBar>
<FilterBarSearch value={draft.query} onValueChange={query => setDraft(s => ({ ...s, query }))} placeholder="이름 검색" />
<Card><CardContent>
<FilterBar layout="list-basic">
<Select value={draft.status} onValueChange={status => setDraft(s => ({ ...s, status }))}>
<SelectTrigger size="medium" aria-label="사용자 상태"><SelectValue /></SelectTrigger>
<SelectContent>
@@ -94,13 +99,15 @@ export function QueryList() {
<SelectItem value="inactive">비활성</SelectItem>
</SelectContent>
</Select>
<FilterBarSearch value={draft.query} onValueChange={query => setDraft(s => ({ ...s, query }))} placeholder="이름 검색" />
<FilterBarActions>
<Button type="button" variant="primary" appearance="solid" size="default" onClick={apply}>조회</Button>
<Button type="button" variant="cancel" appearance="outline" size="default" onClick={reset}>초기화</Button>
<Button type="button" variant="primary" appearance="outline" size="default" onClick={apply}>조회</Button>
</FilterBarActions>
</FilterBar>
<p role="status">조회 결과 {data.length}건</p>
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
</CardContent></Card>
</>
}
```
@@ -119,11 +126,11 @@ Input, Select, Combobox, Date Picker, Button(그 외 Popover 등 자유). 각
`Patterns/Data/Filter Bar` → `Filter Bar`: 검색, 상태 Select, 분류 Combobox, 날짜 Date Picker, 초기화 / 조회, 적용된 조건 수.
대표 Story의 초기화 / 조회는 목록의 반복 조회·보조 작업 세트다. [Button 사용 위치와 버튼 세트 규칙](../components/button.md#5-사용-위치와-버튼-세트)에 따라 둘 다 `appearance="outline"`, `size="default"`를 사용하고 역할만 `cancel` / `primary`로 구분한다. FilterBar는 Button appearance를 강제하지 않으며 소비자가 같은 세트의 appearance와 size를 맞춘다. 대표 Story의 조회는 실행 callback이 없는 조합 예시이고, 적용된 조건 수는 입력 상태에 즉시 반영된다.
현재 대표 Story는 조건→검색어→조회→초기화, 조회 `variant="primary" appearance="solid"`·초기화 `variant="cancel" appearance="outline"`를 사용한다. 이는 [최신 목록 검색 계약](../guidelines/publisher-additional-review-2026-10-10.md)에 따른 Button 세트 예외다. FilterBar 자체가 Button appearance나 조회 시점을 강제하지 않는다. 대표 Story의 입력 조건 수는 즉시 변하지만 조회 적용 조건 수는 조회 클릭에만 반영되며 초기화는 둘 다 비운다. 업무 화면은 §6.1처럼 요청된 동작을 구현하며 Story의 부가 기능을 필수로 복사하지 않는다.
## 10. Accessibility
검색은 `type="search"` + `aria-label`, 아이콘은 `aria-hidden`. 컨트롤마다 `aria-label`을 준다(Story 기준). Reset·Clear는 `button`. wrapper는 시맨틱 변경 없음. 브라우저 기본 검색 지우기 버튼은 숨기고 자체 버튼을 쓴다.
검색은 `type="search"` + `aria-label`, 아이콘은 `aria-hidden`. 컨트롤마다 `aria-label`을 준다(Story 기준). Reset·Clear는 `button`. wrapper는 시맨틱 변경 없음. 지우기 버튼이 사라질 때 검색 입력에 focus를 돌려주며 소비자 ref 전달을 유지한다. 브라우저 기본 검색 지우기 버튼은 숨기고 자체 버튼을 쓴다.
## 11. Responsive
+5 -3
View File
@@ -1,5 +1,7 @@
# GNB (Pattern)
> 2026-10-10 추가 수정 요청 기준: 기존 Reference 콘텐츠 LNB는 본문과 동일 배경·세로 divider 1개, 아이콘 기본 생략(선택적 허용), 해당 컬러 계열 선택·Hover. data-hm-color="brown" 범위는 Brown 색상. 신규 AI GNB_ONLY Sidebar 금지 정책 유지. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
> **SINGLE NAVIGATION MODE.** 독립 GNB / LNB / Dashboard Mode는 함께 사용하지 않는다. 2026-10-08 확정: GNB ONLY는 기존 LNB 부품을 재사용하는 내부 Sidebar를 포함할 수 있다(§15). [공통 정책](../guidelines/navigation-single-mode.md)이 이전 조합 기록보다 우선한다.
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6.10 Pattern 계층, §6.11 Layout 단계 기준)를 따른다.
@@ -94,7 +96,7 @@ ui-viz에서 GNB(Variant B, top-header)와 LNB(Variant A, sidebar)는 서로 대
## 6. Public API
- `GNB`: `header` props + `sticky`(기본 false). `data-slot="gnb"`, sticky면 `data-sticky`.
- `GNBUser`: `button` props + `name`(필수), `description`, children = Avatar(default size, 이미지 실패 시 `AvatarFallback`). `aria-label` 기본 `"<name> 사용자 메뉴"`. `data-slot`은 spread 뒤에 둬서 `DropdownMenuTrigger asChild`가 덮지 않는다.
- `GNBUser`: `button` props + `name`(필수), `description`, children = Avatar(default size, 이미지 실패 시 `AvatarFallback`). 기본 `aria-label`은 `"<name> <description> 사용자 메뉴"`이며 description이 없으면 `"<name> 사용자 메뉴"`다. 명시적 `aria-label`/`aria-labelledby`는 소비자가 지정할 수 있고, 화면에 보이는 이름·설명을 포함해야 한다. `data-slot`은 spread 뒤에 둬서 `DropdownMenuTrigger asChild`가 덮지 않는다.
- `GNBMobileMenu`: 햄버거(`lg:hidden`)와 Sheet. `title`, `side`, `open`/`defaultOpen`/`onOpenChange`. `GNBMobileLink`는 router `preventDefault`가 있어도 닫히도록 context로 직접 닫는다(Radix Close는 prevented click을 건너뛴다).
## 7. Navigation 원칙
@@ -180,10 +182,10 @@ src/patterns/gnb.stories.tsx
- 프레임: `data-hm-gnb-layout="sidebar"`. 소비자가 높이를 지정하며 Grid가 GNB의 자연 높이와 남은 본문 높이를 배분한다.
- 본문: 프레임의 직계 `data-hm-gnb-body`. Desktop은 기존 `--hm-lnb-width` 열 + 가용 콘텐츠 열, Mobile은 콘텐츠 한 열이다.
- Sidebar: 본문의 직계 `<LNB data-hm-gnb-sidebar>`. 기존 `data-slot="lnb"`는 유지한다. 이 범위의 `--hm-lnb-background`만 `var(--hm-sem-palette-green-light-2)`로 덮어쓴다. 기본 Green Light 2는 `#266651`이며 Runtime Theme override도 그대로 따른다. 측면 접점 배경은 LNB의 기존 `--hm-lnb-background-end`를 유지한다.
- Sidebar: 본문의 직계 `<LNB data-hm-gnb-sidebar>`. 기존 `data-slot="lnb"`는 유지한다. 이 Reference 범위에서 본문과 같은 배경, 세로 divider 한 개를 사용한다. Green은 Primary, `data-hm-color="brown"`은 기존 Brown 팔레트로 선택·Hover를 표현한다. 펼침 화살표는 유지하며 아이콘은 선택적으로 전달한다. 신규 AI GNB_ONLY에는 Sidebar를 추가하지 않는다.
- Main: 본문의 직계 `<main data-hm-gnb-main>`. 공용 CSS가 면·접점·스크롤을 제공하며 콘텐츠 padding은 소비자가 정한다. GNB의 상단 접점과 LNB의 측면 접점은 기존 값을 재사용한다.
메뉴 폭·폰트·높이·padding·gap·아이콘·Depth·Hover·선택·연결선·스크롤은 LNB 그대로다. 반투명 Hover/Open 면의 합성 결과만 배경 변경에 따라 달라질 수 있으며 별도 보정하지 않는다. 그룹은 기존 Collapsible의 `open / onOpenChange`를 사용하고 GNB NavigationMenu 열림과 독립적으로 관리한다. 선택 시 새 자동 펼침을 추가하지 않는다.
메뉴 폭·폰트·높이·padding·gap·Depth·스크롤은 LNB 계약을 재사용한다. 이 콘텐츠 Reference에만 아이콘 기본 생략, 동일 배경·세로 구분선 한 개, 해당 컬러 계열 Hover·선택을 적용하고 Green LNB의 오목한 선택 접점은 사용하지 않는다. 그룹은 기존 Collapsible의 `open / onOpenChange`를 사용하고 GNB NavigationMenu 열림과 독립적으로 관리한다. 선택 시 새 자동 펼침을 추가하지 않는다.
Desktop은 ≥1024px, Mobile은 ≤1023px이다. Mobile에서 Sidebar는 기존 LNB CSS로 숨기고 **GNBMobileMenu 하나**에 `LNBMobileGroup / LNBMobileLink` 목록을 넣는다. `LNBMobileHeader`나 `LNBMobileMenu`를 함께 렌더하지 않는다. 모바일 Sheet 기본 면, 44px 링크, 14/20/500(current 700), 중첩 들여쓰기, Escape·포커스 복귀를 유지한다.
+19 -3
View File
@@ -1,5 +1,9 @@
# LNB (Pattern)
> 2026-10-10 추가 수정 요청 기준: 독립 Green LNB 펼친 2Depth 그룹 배경만 Black 8%. 글자·아이콘 opacity와 선택·Hover 보존. GNB 콘텐츠 LNB는 별도 밝은 배경 범위. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
> 2026-10-10 추가 확정: Storybook 왼쪽 목차의 펼친 2Depth 그룹 배경도 Black 8%로 적용합니다. 컴포넌트는 [HM LNB CSS](../../packages/ui/src/theme/lnb.css), 목차는 [Storybook Manager CSS](../../.storybook/manager.css)의 기존 펼친 그룹 selector가 각각 담당합니다. 배경 Alpha만 적용하며 글자·아이콘 opacity, Selected·Hover·Focus, 검색·필터·펼침·접힘은 유지합니다. Selected inset depth 등에 쓰는 별도 Black 20%는 그룹 배경이 아니므로 유지하며, 공통 Theme·Token을 변경하지 않습니다.
> **2026-10-07 확정: SINGLE NAVIGATION MODE.** GNB / LNB / Dashboard는 함께 사용하지 않는다. AI 코드 생성·제안, 신규/기존 페이지, 프로토타입, Storybook, Production 전체에 적용한다. [공통 정책](../guidelines/navigation-single-mode.md)이 이전 조합 기록보다 우선한다.
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6.10, §6.11)를 따른다.
@@ -156,7 +160,7 @@ LNB Mode가 Desktop Brand/Profile과 Mobile Header/Sheet Profile을 담당한다
| 항목 | 18/28/700, 최소 56, padding 14 12 14 18, White, 아이콘 20 opacity .5, 글자 테두리 1.5px + 그림자 `1px 2px 1px` | 같음 | MATCHED |
| hover | Black 10%, 콘텐츠 경계까지, 시작 쪽 radius 4 | 같음 | MATCHED |
| current | Beige + `#123328`, 왼쪽 radius 8, 오른쪽 20+1 연장, 위·아래 18 오목 곡선, 안쪽 깊이 그림자(마지막 25% fade), 글자 효과 없음 | 같음. 깊이는 span 대신 background gradient 3겹으로 근사 | MATCHED / IMPLEMENTATION_CHANGE |
| nested(2뎁스) | 상위 = disclosure 버튼, 화살표 접힘 → 펼침 90°, 펼친 그룹 Black 20% + 1px `#266651` 선 + 왼쪽 radius 8, 하위 padding-left 68, 하단 8, 하위 선택 시 상위 노랑 `#FFC600` | 같음. HM Collapsible(Radix) 사용 | MATCHED / IMPLEMENTATION_CHANGE |
| nested(2뎁스) | 상위 = disclosure 버튼, 화살표 접힘 → 펼침 90°, 펼친 그룹 Black 8% + 1px `#266651` 선 + 왼쪽 radius 8, 하위 padding-left 68, 하단 8, 하위 선택 시 상위 노랑 `#FFC600` | 같음. HM Collapsible(Radix) 사용 | MATCHED / IMPLEMENTATION_CHANGE |
| divider | Brand/Profile 구분선 없음, 그룹 구분선은 그룹이 둘 이상일 때만(현재 없음) | 없음 | MATCHED |
| profile | 이름 16 + 역할 12, avatar 없음, 아래 22 | 위치·글자 같음 + Avatar 32px(gap 12, 세로 중앙) | INTENTIONAL_CHANGE |
| 외곽 brand 테두리(`.sidebar::after`) | D-093 예시에 없음, FOLLOW_UP | 그리지 않음 | FOLLOW_UP |
@@ -176,7 +180,7 @@ LNB Mode가 Desktop Brand/Profile과 Mobile Header/Sheet Profile을 담당한다
| `--hm-lnb-background` / `-background-end` | 5-stop gradient / `#266651` |
| item | min-height 56, 18/28/700, gap 12, 아이콘 20, 글자 테두리 1.5px |
| current | `--hm-sem-bg-content` + `--hm-sem-action-primary`, radius 8 0 0 8 |
| group | open face Black 20%, 선 1px `--hm-sem-palette-green-light-2`, 하위 padding-start 68, current 글자 `--hm-sem-palette-yellow-accent` |
| group | open face Black 8%, 선 1px `--hm-sem-palette-green-light-2`, 하위 padding-start 68, current 글자 `--hm-sem-palette-yellow-accent` |
| `--hm-lnb-third-*` | min-height 48, padding-block 12, text padding-start 88(메뉴 행 기준), line-height 24, weight 500 / current 700, connector x `= --hm-lnb-subitem-padding-start`(68)·length 12·width 1, 모든 상태에서 `--hm-sem-palette-green-tint` 40%. 실제 link는 connector 끝부터 시작 |
새 Primitive: `--hm-yellow-accent: #ffc600`(`primitives.css`). ui-viz `color.action.accent-on-green`이며 HM 팔레트에 없어 추가했다. 그 외 새 Semantic은 없다.
@@ -188,9 +192,21 @@ LNB Mode가 Desktop Brand/Profile과 Mobile Header/Sheet Profile을 담당한다
- 높이는 소비자가 정한다(열은 `min-height: 100%`).
- GNB 또는 Dashboard와 조합하지 않는다. 콘텐츠 계산은 LNB 자체 변수만 사용한다.
### 내부 스크롤과 숨김 알림 Usage
viewport 높이 shell에서 모바일 Header와 main을 세로 flex로 배분할 때 부모와 main의 `min-height: 0`, main의 `flex: 1; overflow: auto`를 확인한다. 위 콘텐츠 면의 `position: relative`는 main 안의 absolute 숨김 status가 BODY를 기준으로 배치되어 바깥 빈 스크롤을 만드는 것도 방지한다. 숨김 알림은 Toast가 아니며 요청 없이 시각 알림 기능을 추가하지 않는다.
```css
/* 소비 레이아웃의 높이·스크롤만 구성한다. 기존 HM 면·간격은 유지한다. */
.content-stack { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.page-content { position: relative; flex: 1; min-height: 0; min-width: 0; overflow: auto; }
```
390/768/1280/1440px에서 초기·조회·초기화·완료 후 `document.documentElement.scrollHeight/clientHeight`, main의 같은 값, live region의 `offsetParent`·크기·문구를 기록한다. viewport 높이 shell이면 바깥 스크롤 없이 main 내부의 마지막 항목에 접근할 수 있어야 한다. 일반 문서 흐름 화면에는 viewport 높이를 강제하지 않는다. 완료 후 disabled 버튼 대신 유효한 focus 대상이 유지되는지도 확인한다. 전역 overflow:hidden은 해결책이 아니다.
## 7. Accessibility / 상호작용
`<aside>` + `<nav aria-label>`, 현재 `aria-current="page"`, 그룹 trigger는 Radix `aria-expanded`/`aria-controls`. `LNBProfile`은 native button + 기본 accessible name `"<name> 사용자 메뉴"`이며 Avatar·이름·역할의 inner content 폭에 interaction을 한정한다(`fit-content` + 안쪽 hit padding). 이 버튼에 pointer cursor와 LNB focus-visible outline을 쓴다. `DropdownMenuTrigger asChild`로 합성하면 Enter/Space 열기, Arrow 이동, Escape 닫기와 trigger focus 복귀는 Radix가 담당한다. 접힌 그룹의 하위는 DOM에서 빠져 focus되지 않는다. 아이콘은 장식(`aria-hidden`, 소비자가 지정).
`<aside>` + `<nav aria-label>`, 현재 `aria-current="page"`, 그룹 trigger는 Radix `aria-expanded`/`aria-controls`. `LNBProfile`은 native button + 기본 accessible name `"<name> <description> 사용자 메뉴"`이며 description이 없으면 `"<name> 사용자 메뉴"`다. 명시적 `aria-label`/`aria-labelledby`를 소비자가 지정할 때도 화면에 보이는 이름·설명을 포함한다. Avatar·이름·역할의 inner content 폭에 interaction을 한정한다(`fit-content` + 안쪽 hit padding). 이 버튼에 pointer cursor와 LNB focus-visible outline을 쓴다. `DropdownMenuTrigger asChild`로 합성하면 Enter/Space 열기, Arrow 이동, Escape 닫기와 trigger focus 복귀는 Radix가 담당한다. 접힌 그룹의 하위는 DOM에서 빠져 focus되지 않는다. 아이콘은 장식(`aria-hidden`, 소비자가 지정).
## 8. 검증 (2026-10-03, Storybook)
+13 -3
View File
@@ -1,5 +1,7 @@
# Page Header (Pattern)
> 2026-10-10 추가 수정 요청 기준: 기본형은 Breadcrumb 없이 제목 중심. 설명·액션·Breadcrumb는 선택 적용. 대표 Story에서 두 구성을 비교하며 D-01/D-02는 유지. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
현재 HM 구현의 기준 문서. 공통 원칙은 [프로젝트 지침](../guidelines/프로젝트-지침.md), [Button 규칙](../components/button.md), [단일 Navigation 정책](../guidelines/navigation-single-mode.md)을 따른다.
```text
@@ -73,19 +75,27 @@ Story의 Breadcrumb hash 링크와 등록 버튼은 조합 예제다. 실제 화
Breadcrumb는 HM Breadcrumb 그대로(14px, 링크). ui-viz는 12px 정적 텍스트였다(INTENTIONAL_CHANGE).
### 3.1 제목 아래 Tabs를 배치할 때
페이지 content 면에 바로 놓는 Tabs는 PageHeader 다음에 기본 Primary(`panelSurface="content"`)를 사용한다. PageHeader의 제목→설명 **8px**, 아래 첫 콘텐츠까지 **20px**을 유지한다. 탭을 감싸는 새 White Card·border·padding은 필요하지 않으며 내부 `TabsContent`는 `px-0 py-6`로 조합할 수 있다. [Tabs의 표면·여백 소유권](../components/tabs.md#16-publisher--developer-조합-안내-2026-10-09)을 따른다.
2026-10-09 실제 반영: 대표 Story의 Overview에서 기존 PageHeader 바로 뒤에 `내 업무 / 완료 업무` Tabs와 공개 Table을 조합한다. 선택 탭의 면은 [Primary 상단 White alpha 0.8](../components/tabs.md#163-d-02-상단-흰색-80-실제-반영) → content로 이어지고, 탭 전체 opacity는 1이다. PageHeader Component·Typography·8px/20px Token과 기존 단일 Brand Action은 유지한다. 별도 Pattern API를 추가하지 않는다.
이때 TabsContent의 상하 24px을 PageHeader 하단 20px에 더해 탭 바 앞 간격으로 사용하지 않는다. Card 안의 제목은 CardTitle 규격(20/28/600), 페이지 제목은 PageHeaderTitle 규격(28/36/700)을 사용한다. CardDescription과 PageHeaderDescription은 서로 다른 계약이다. 비교 이미지의 48px 간격이나 Card 전용 Token을 페이지 전체에 적용하지 않는다.
## 4. Responsive / Accessibility
`PageHeaderContent`는 `flex: 1 1 320px; min-width: 0`, 액션은 줄바꿈 후 오른쪽 정렬(390px에서 제목 아래 줄). 긴 제목은 `overflow-wrap: anywhere`. 화면당 `h1`은 하나만 둔다. wrapper에 role을 두지 않는다.
PageHeader는 콘텐츠의 제목 영역이며 Navigation Mode를 정의하지 않는다. 독립 대표 Story는 Navigation을 추가하지 않은 조합 예제다. 실제 페이지는 `GNB | LNB | DASHBOARD` 중 하나만 선택하고 반응형에서도 유지한다. Dashboard에서는 Dashboard 내부 Header/Menu/Sidebar와 합성하며 외부 GNB/LNB를 함께 넣지 않는다.
PageHeader는 콘텐츠의 제목 영역이며 Navigation Mode를 정의하지 않는다. 독립 대표 Story는 Navigation을 추가하지 않은 조합 예제다. 실제 페이지는 `GNB | LNB | DASHBOARD` 중 하나만 선택하고 반응형에서도 유지한다. Dashboard 운영 화면에서는 전용 Header/업무 상태 필터/Content 안에 합성하며 Sidebar나 외부 GNB/LNB를 함께 넣지 않는다.
Dashboard Compact 최소 폭 960px(Sidebar 포함 1232px)과 내부 가로 스크롤은 Dashboard Layout 계약이다. PageHeader 단독 사용처에 이 최소 폭을 자동 적용하지 않는다. PageHeader 자체에는 고정 최소 폭이나 별도 breakpoint가 없으며 기존 flex wrapping과 Theme 규격을 유지한다. 외부 콘텐츠 padding은 소비 Layout이 소유한다.
Dashboard 운영 화면은 전용 48px Header·업무 상태 필터·Content로 구성하고 Sidebar 없이 최소 폭 960px·내부 가로 스크롤을 유지한다. Sidebar 포함 1232px은 기존 호환성·verification 구성만의 값이다. PageHeader 단독 사용처에 Dashboard 최소 폭을 자동 적용하지 않는다. PageHeader 자체에는 고정 최소 폭이나 별도 breakpoint가 없으며 기존 flex wrapping과 Theme 규격을 유지한다. 외부 콘텐츠 padding은 소비 Layout이 소유한다.
## 5. 검증 / 관련 파일
### 현재 대표 Reference
- [독립 대표 Story 소스](../../src/patterns/page-header.stories.tsx): Breadcrumb + Title + Description + Actions. 운영 조합에 별도 Story CSS나 Dashboard 최소 폭을 추가하지 않는다.
- [독립 대표 Story 소스](../../src/patterns/page-header.stories.tsx): Breadcrumb + Title + Description + Actions + Primary Tabs / Table의 직접 배치 조합. 운영 조합에 별도 Story CSS나 Dashboard 최소 폭을 추가하지 않는다.
- Storybook 경로: `Patterns/Layout/Page Header → Page Header`, ID: `patterns-layout-page-header--all`.
- 기존 Dashboard 대표 Story는 Dashboard 전체 Layout의 Reference로 유지한다. Verification Story는 운영 Reference로 사용하지 않는다.