# Tabs
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Tabs 고유의 API·Token·상태·결정만 기록한다.
## 기본 사용: Primary (2026-10-08 기본 API / 2026-10-09 조합 안내)
앱 진입점에서 `@hm-design/ui/theme.css`를 로드한다. `TabsList`의 기본값은 `fill=true`, `panelSurface="content"`이며 기본 API만으로 Primary가 적용된다.
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@hm-design/ui"
개요
상세
개요 콘텐츠
상세 콘텐츠
```
- **Primary**: ``, ``, ``는 같은 디자인이다. 실제 content 면 위에서 사용하며 선택 면은 White alpha **0.8** → `--hm-sem-bg-content` Gradient다. 탭 전체 opacity는 1이다.
- **Secondary 명시적 사용**: 흰색 Card 안에서는 List를 ``로 바꾸고 **각** `TabsContent`의 `px-0 py-6`를 유지한다. 바깥 Card가 White surface와 여백을 소유하므로 내부 패널에 별도 박스를 추가하지 않는다.
- **Legacy non-fill 명시적 사용**: ``와 **각** `TabsContent`의 `border bg-card rounded-b p-4` 조합으로 이전 스타일을 유지한다. ``도 같은 시각이다. OLD_TEST_ONLY / REMOVE_CANDIDATE 이력은 유지하며 별도 승인 없이 삭제하지 않는다.
`TabsContent`는 패널 배경·테두리를 자동으로 지정하지 않는다. Primary의 바탕은 페이지/Layout의 content 면(`bg-background`), Secondary는 상위 Card의 surface 면이다. `panelSurface`는 List의 선택 면·연결 곡선에만 적용되며 콘텐츠 색을 바꾸지 않는다. Bar와 패널 사이에 gap이나 별도 구분선을 넣지 않는다. 상태·Variant는 §4.1, 외부 소비자 변경 안내는 §15, 중첩 박스 없는 조합은 §16을 따른다. 2026-10-09 실제 구현 요청에 따라 Primary 상단 흰색 불투명도 80%를 해당 Tabs CSS Mapping에 반영했다(§16.3).
## 1. 상태
| 항목 | 내용 |
|---|---|
| 구현 상태 | 기본 Primary Fill + 명시적 Secondary Fill + Legacy non-fill + 넘침 이동 버튼(D-090) 구현 (`hm-tab` class 없음) |
| 기준일 | 2026-10-09 (D-01 조합·D-02 상단 White 80% 반영, API는 2026-10-08 기본값 유지) |
| 기반 | shadcn/ui Tabs (Radix `Tabs`) |
| 대표 Story | `Components/Navigation/Tabs` → `All` (이름 "Tabs"): Primary(Fill + content) / Secondary(Fill + surface) |
| 보류 | 아래 §11 |
## 2. 적용 근거
- **참고 근거**: ui-viz 기존 테스트 규격(`docs/components/tabs.md`, `brand/hanmac/components/tab.md`)의 기본 profile, `fill` 콘텐츠 전환 profile, 넘침 이동 버튼의 디자인 의도와 수치.
- **현재 HM 규격**: Primary / Secondary는 §4.1, Legacy non-fill은 §3~§4. 수치는 ui-viz에서 가져왔지만 HM 구현에서 다시 검토해 유지한 것이다(아래 검토 결과).
- **구현 방식**: shadcn / Radix Tabs 구조(roving focus, 방향키·Home/End, 자동 활성화, ARIA)를 유지하고 theme CSS로 HM 스타일을 연결. `TabsList`가 frame과 이동 버튼을 렌더.
- **보류**: §11.
**ui-viz 비확정 값 검토 (2026-10-02)**: Default 면(`green-tint` 45%)·Default 테두리(`green-light-2` 35%)는 ui-viz에서 비확정 표기였으나 HM Primitive 팔레트의 값을 투명도로 합성한 것이고 Selected(White)와의 대비가 분명해 HM 방향에 맞는다고 판단, 현재 HM 규격으로 유지한다. 곡선 반경 8px도 같은 이유로 유지한다. 단 값은 디자인 확정 시 조정될 수 있다. ui-viz의 볼록 gradient(미구현), 13px 라벨(미채택)은 가져오지 않았다.
## 3. Legacy non-fill 규격 (`fill={false}`)
이 표의 배치·인접 경계는 이전 non-fill profile이다. 높이·padding·radius·Typography·Bar 규격은 Fill에서도 공유하지만, 균등 폭·인접 경계·선택 면·연결 곡선은 §4.1을 따른다.
| 항목 | 값 |
|---|---|
| Tab 높이 | 34px (`height` + `min-height`), Button·radius token과 alias 없음 |
| Padding | `0 16px` |
| Radius | `4px 4px 0 0` |
| Border | 1px. 인접 탭은 `border-left: 0`으로 단일 경계 |
| Typography | 16 / 24 / 700 (Selected 동일, 구분은 색·면·테두리로) |
| 배치 | `flex-end` 정렬, gap 0, nowrap, `flex: none`, 각 탭 `position: relative` |
| Bar | 자체 표면·padding·border·radius 없음, `margin-bottom: -1px`, `overflow-x: auto`, scrollbar 숨김, `max-width: 100%` |
크기·variant 축 없음 (ui_viz Out of Scope). 방향은 horizontal 하나.
## 4. Legacy non-fill 상태 규격 (`fill={false}`)
`panelSurface="surface"` / `"content"`는 이 상태표에 시각적 영향을 주지 않는다. 아래 Default는 비선택 상태를 뜻하며 기본 Variant를 뜻하지 않는다.
| 상태 | 면 | 테두리 | 전경 |
|---|---|---|---|
| Default (`data-state="inactive"`) | `rgba(189,223,212,.45)` | top/right `rgba(38,102,81,.35)`, bottom `#DEDBD2` | `#266651` |
| Selected (`data-state="active"`) | `#FFFFFF` (`--hm-sem-bg-surface`) | top/right/left `#DEDBD2`, bottom 1px 표면색(기준선을 덮어 콘텐츠와 연결), `z-index: 2` | `#123328` |
| Hover (비선택만, `:hover`) | 기본 면 + Black 20% overlay (`linear-gradient`) | 변화 없음 | `#123328` |
| Hover (선택) | 적용 안 함 | — | — |
| Focus (`:focus-visible`) | shadcn ring 유지 | — | — |
| Disabled | shadcn 기본 `opacity-50`, 클릭 불가 | — | — |
전환: 면·전경 0.15s ease (테두리·geometry는 전환하지 않음). Selected 하단 stroke는 indicator가 아니라 연결 수단이다.
### 4.1 현재 Fill 디자인: Primary / Secondary State · Variant
- **Primary (기본값)**: `fill=true`, `panelSurface="content"`. Selected는 White alpha 0.8 → bg-content Gradient, top / left / right는 Green Light 3(`--hm-tab-fill-line-color`), bottom은 content 패널색이다. 전체 탭 opacity와 전경·경계는 변경하지 않는다.
- **Secondary**: `fill=true`, `panelSurface="surface"`. Selected는 기존 White surface 면, 같은 Green Light 3 테두리, bottom은 surface 패널색이다.
- **공통 Geometry**: 기존 높이 34px, Typography 16/24/700, 좌우 padding 16px, 상단 radius 4px, 선택 탭의 8px 곡선 연결을 유지한다. Overflow가 없으면 균등 폭, 모든 Fill 이웃은 1px left border와 `margin-left: -1px`로 경계를 공유한다. Overflow이면 라벨 필요 폭을 유지한다.
- **Inactive**: 면·top / vertical border·전경은 §4의 기존 비선택 값, bottom line만 Green Light 3다.
- **Hover**: 비선택·활성화 가능한 탭에만 기존 Black 20% overlay와 전경 전환을 적용한다. Selected Gradient / White 면은 유지한다.
- **Focus / Disabled**: 기존 shadcn focus-visible ring / Disabled opacity 0.5·클릭 불가·Radix 키보드 건너뛰기를 유지한다. 선택·ARIA·키보드 동작은 §7, Overflow 이동은 §5·§8을 따른다.
## 5. API
shadcn/Radix 그대로. `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` (Radix `Tabs.Root/List/Trigger/Content` props). 추가(기존 API 무변경, 모두 선택):
```
TabsList:
fill?: boolean 기본 true. Primary / Secondary Fill; false는 Legacy non-fill
panelSurface?: "surface" | "content" 기본 "content". Fill 선택 면이 이어지는 panel 바탕 (임의 색 미지원 = TABS_PANEL_SURFACE_ENUM)
```
- `TabsList`는 Radix list를 감싸는 frame(`data-slot="tabs-frame"`)을 렌더한다(ui_viz `.hm-tabs-frame`과 같은 구조). `role`·`aria-*`·`className`·ref·나머지 props는 tablist로 간다. frame에 `data-overflow`, `data-fill`, `data-panel-surface`.
- `panelSurface`는 `fill=false`이면 시각적으로 무시한다. Props 이름·타입·선택 여부와 공개 export는 유지하며 기본 렌더링만 변경한다(§15).
- 넘칠 때만 frame 안에 이동 버튼 2개(`data-slot="tabs-arrow"`, `aria-label` 「이전 탭 보기」「다음 탭 보기」, `aria-controls`=tablist id)를 tablist 밖에 렌더한다. Tab 순서: 이전 버튼 → 선택 tab → 다음 버튼, 끝 방향 버튼은 `disabled`.
- `value`/`defaultValue`/`onValueChange`, `disabled`, `activationMode`, `orientation` 등은 Radix 동작 그대로다. ui_viz의 controlled-only `` API는 복제하지 않는다.
- 출력 속성: `data-slot="tabs|tabs-list|tabs-trigger|tabs-content"`, Radix `data-state`·`data-orientation`.
- `TabsList`에는 `aria-label` 또는 `aria-labelledby`를 넘겨야 한다(ui_viz §6).
- 이동 버튼은 기존 `Button`을 쓰지 않는 Tabs 전용 control이다.
- Selected가 이어지는 콘텐츠 표면은 소비자의 panel이다. Tabs가 자체 표면·구분선을 만들지 않는다. Tab Bar와 콘텐츠 사이에 구분선을 넣으면 연결이 깨진다.
## 6. Token
Component Mapping 위치: `packages/ui/src/theme/tabs.css` (`--hm-tab-*`).
- Tabs 전용: `--hm-tab-{height,padding-x,radius-top,border-width}`, `--hm-tab-font-{size,weight}`, `--hm-tab-line-height`, `--hm-tab-{background,border-color,text}`, `--hm-tab-selected-{background,border-color,text}`, `--hm-tab-hover-{overlay,text}`, `--hm-tab-transition-duration`.
- 재사용한 공통 값: `--hm-sem-bg-surface`(Selected 면), 역할 Semantic `--hm-sem-action-primary`, `--hm-sem-stroke-input-default`, Theme palette `--hm-sem-palette-green-tint`, `--hm-sem-palette-green-light-2`, `--hm-sem-palette-black`. Component Token은 독립적으로 유지한다. Primitive 직접 참조 설명은 초기 구현 이력이다.
- 이동 버튼: `--hm-tab-arrow-{size,icon-size,background,border-color,icon-color,disabled-opacity}`. fill: `--hm-tab-fill-line-color`(→ `--hm-sem-palette-green-light-3`), `--hm-tab-fill-curve-radius`(8px, REVIEW VALUE), 내부 `--hm-tab-fill-{panel,face}`.
- **초기 Primitive 추가 이력**: `--hm-green-light-3: #249473` (ui-viz 팔레트의 Green Light 3). 현재 Tabs fill은 공개 Theme palette `--hm-sem-palette-green-light-3`를 통해 참조한다.
## 7. shadcn / Radix에서 유지한 항목
Radix Tabs 구조·ARIA(`role=tablist/tab/tabpanel`, `aria-selected`)·roving tabindex·방향키 순환·Home/End·자동 활성화, `data-slot`, `cn(..., className)`, trigger의 focus ring, Disabled 처리.
## 8. HM에서 변경한 항목
- shadcn의 list `bg-muted`·rounded·padding, trigger의 흰색 pill/dark 스타일, root `gap-2`, list variant(`default`/`line`) 제거 → theme CSS가 담당.
- Bar는 콘텐츠와 gap 없이 붙는다.
- `TabsList`가 frame wrapper와 이동 버튼을 렌더(ui_viz 구조 반영, 프로젝트 지침상 필요한 wrapper).
- 선택 tab을 보이게 유지: click/keyboard 선택, resize·글꼴 로딩·항목 변경 시 list `scrollLeft` 보정(ResizeObserver/MutationObserver).
## 9. Storybook
- 대표 Story `All`("Tabs"): **Overview**는 fill / panelSurface를 생략한 기본 Primary, **Default API**는 `fill`만 명시한 Primary와 `fill` + `panelSurface="content"`를 명시한 Primary를 비교한다. **Variants**는 명시적 Secondary, **States**는 기본 Primary + 8개 탭 + 260px wrapper Overflow다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다.
- verification(`tags: ["verification"]`): `Secondary`, `SelectedFirst`, `SelectedLast`, `Focus`, `Disabled`, `Overflow`, `SecondaryOverflow`, `LongLabel`. Primary 기준이며 Secondary는 같은 CSS 경로를 쓴다. Focus는 `play`로 실제 focus를 준다.
- `LongLabel`은 짧은 라벨, 긴 라벨 1개, 긴 라벨 여러 개, 260px 좁은 container를 함께 검증한다. 라벨은 `nowrap`·무말줄임이며 실제 필요 폭이 넘을 때 이동 버튼으로 전환한다.
- non-fill Story(`Selected`, `SelectedMiddle`, `Fill`, `FillContent`, `FillOverflow`와 대표의 non-fill 구역)는 2026-10-06에 제거했다. API(`fill=false`, Legacy profile CSS)는 유지한다(§10).
- 2026-10-08 숨김 `Compatibility`(`verification` 태그 + **VERIFICATION ONLY** 표시)는 Legacy `fill={false}` / `fill={false} panelSurface="surface"` 동일성과 `panelSurface="surface"`만 명시한 경우의 Secondary 전환을 확인한다. 일반 Story 메뉴를 추가하지 않는다.
- Overview / Default API / Variants / States 및 Fill verification 예제의 콘텐츠 panel은 `px-0 py-6`로 통일해 D-01을 실제로 반영한다. Primary는 상위 content 면, Secondary는 상위 surface 면에 연결하며 내부 패널의 별도 border·배경·radius를 제거한다. Compatibility의 Legacy non-fill 두 예제만 이전 `border bg-card rounded-b p-4`를 보존한다.
- **Usage**(2026-10-09 추가)는 공개 Card / Input / Textarea / Table을 조합해 Card 내부 Secondary와 페이지 content 면 위 Primary를 보여준다. 두 예제 모두 `TabsContent`는 `px-0 py-6`이며 별도 배경·border·radius를 추가하지 않는다. 대표 Story ID·기존 구역·verification Story는 유지한다.
## 10. 사용자 확정 결정
- 2026-10-06: Tabs의 최종 디자인 언어는 **Primary = Fill + `panelSurface="content"`**(기본형), **Secondary = Fill + `panelSurface="surface"`**(서브형) 두 가지다. 이 결정이 ui_viz보다 우선한다. non-fill 기본 profile은 OLD_TEST_ONLY로 분류해 대표 Story에서 내렸다.
- **2026-10-06 당시 범위**는 Storybook 대표형에 한정했다. 당시 Public API 기본값(`fill=false`, `panelSurface="surface"`)과 non-fill CSS branch는 유지하고 기본값 변경·branch 삭제를 별도 승인 대상으로 남겼다. 당시 저장소 내 소비처가 없다는 관찰은 외부 소비자의 무영향을 보장하지 않는다.
- **2026-10-08 추가 개선 E 승인**: `TabsList` 기본값을 `fill=true`, `panelSurface="content"`로 변경한다. Primary / Secondary / Legacy의 기존 CSS 디자인 값·선택자·동작·Theme / Token을 유지한다. non-fill의 OLD_TEST_ONLY / REMOVE_CANDIDATE 이력과 명시적 사용 API는 유지하며 삭제 승인은 포함하지 않는다. `panelSurface`는 Fill일 때만 효과가 있는 List prop이다(`Tabs` Root prop이 아님).
- **2026-10-09 실제 구현 요청**: 사전공유 D-01/D-02를 문서 외 실제 구현에도 적용한다. Fill Story의 중첩 패널 박스를 제거하고 PageHeader 대표 Pattern에 직접 Tabs를 조합한다. 기존 `tabs.css`의 `fill=true` + `panelSurface="content"` 내부 face gradient 상단만 80%로 바꾼다. Secondary·Legacy·공개 API·Theme JSON·공통 Palette 변경은 포함하지 않는다.
## 11. 미확정 / 보류
- 이동 후 가장자리 정렬 세부 (다음 버튼은 오른쪽에 걸친 첫 항목의 끝을 보이는 영역 끝에 맞춤), 이동 버튼 포커스 수치(global focus TBD, box-shadow 3px 링 사용), fill에서도 Bar −1px 겹침 유지, Brown 콘텐츠 탭·dark mode TBD.
- `panelSurface`는 `surface`/`content`만 지원(현재 제한).
- Default 면·테두리 합성색과 곡선 반경 8px는 디자인 확정 시 재검토.
- Focus 수치: global focus `TBD`. shadcn ring 유지.
- Disabled: ui_viz P0 제외. shadcn 기본.
- 볼록 Gradient: `TABS_CONVEX_GRADIENT_KNOWN_DEVIATION`, 평면으로 출하(구현도 평면).
- Typography: expanded Source의 13px는 `TABS_SOURCE_LABEL_SIZE_13_UNADOPTED`로 16px 유지.
- 앞 탭 right border가 2번째 이후 Selected의 좌측을 대신 그리는 관계는 ui_viz `BASELINE_BEHAVIOR_ACCEPTABLE`이며 그대로 구현.
## 12. 관련 파일
```
packages/ui/src/components/ui/tabs.tsx
packages/ui/src/theme/tabs.css
packages/ui/src/theme.css (import)
packages/ui/src/index.ts (export Tabs, TabsContent, TabsList, TabsTrigger)
src/components/tabs.stories.tsx
src/components/story-board.tsx
```
## 13. 검증 상태
- **2026-10-09 D-01/D-02 실제 반영**: 전체 typecheck·앱 build·Storybook build 통과. 1280×900 / 390×844의 Primary 선택 면 상단 `color(srgb 1 1 1 / 0.8)`·하단 불투명 content·전체 opacity 1, Secondary 불투명 White, Fill panel transparent/border 0/padding 24px 0을 확인했다. PageHeader Pattern의 8px·20px과 실제 조합, Legacy·Disabled·키보드·넘침 이동·긴 라벨·입력 초안 보존을 확인했다. 참고 원본/비대상 파일과 API는 보존했으며 상세 범위·수치는 §16의 통합 보고를 따른다. 아래 과거 검증 기록은 해당 날짜의 이력이다.
- 2026-10-08 추가 개선 E: 기존 6024 Storybook 재사용. 대표 `All`에서 기본 Props / `fill`만 명시 / 두 Props 명시가 모두 Primary(`data-fill="true"`, `data-panel-surface="content"`)이며 같은 Gradient·Green Light 3 경계·34px·16/24/700·8px 곡선을 확인했다. Secondary의 White 면·패널과 Bar / panel 1px 겹침도 유지했다.
- 숨김 `Compatibility`: `fill={false}`의 content / surface 조합은 같은 White 선택 면·Legacy 경계·라벨 필요 폭이고 곡선이 없다. `panelSurface="surface"`만 명시하면 Secondary다. CSS 주석을 제외한 모든 규칙·값과 Production의 기본값 두 개를 제외한 코드가 작업 시작 상태와 동일함을 비교했다.
- 실제 포인터 / 키보드: 클릭·ArrowLeft / Right·Home / End 선택과 콘텐츠 전환, 비선택 Hover Black 20% / 선택 Gradient 유지, focus-visible 3px 링, Disabled opacity 0.5·pointer-events none·tabIndex -1·방향키 건너뛰기를 확인했다. 독립 Overflow Story는 260px frame / 192px list에서 다음 이동 `scrollLeft 0→33.33`, 이전 이동 `→0`, 끝 버튼 Disabled 전환과 선택 유지가 정상이다.
- 1280px / 390px: 대표 Story의 문서 가로 넘침 없음. 390px의 기본·명시적 Fill frame은 약 342.67px, Overflow specimen은 260px. 390px Compatibility도 문서 가로 넘침 없이 Legacy list 약 226px를 유지했다. HM Theme의 content / surface / Green Light 3 연결은 유지한다.
- `npm run build:ui`(UI 패키지·선언 빌드), `node node_modules/typescript/bin/tsc -p tsconfig.app.json`(Story 포함) 통과. Tabs 변경 파일의 diff whitespace 검사 통과. 전체 회귀·전체 Storybook 정적 빌드는 이번 범위에서 실행하지 않았다.
- Console: 패키지 dist 재생성 중 기존 브라우저에 Vite `src/index.css` reload 오류가 한 번 기록됐으나 복구됐다. 빌드 후 새 브라우저에서 대표 / Compatibility를 확인한 Console error / warning은 0건이다. 외부 소비자 전체 호환성은 §15의 한계를 따른다.
- 2026-10-04: `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. build의 기존 Tailwind 문서 예시 경고(`bg-[var(...)]`)와 Storybook 500kB chunk 경고 외 신규 경고 없음.
- Storybook computed style(`components-navigation-tabs--all`): 높이 34px, padding `0 16px`, radius `4px 4px 0 0`, 16/24/700, Default/Selected 색과 연결선 일치. fill 선택 경계 `rgb(36,148,115)`, 선택 면과 8×8 곡선 확인.
- 실제 pointer + keyboard: 비선택 hover Black 20% overlay·전경 전환, 선택 hover 비적용, 클릭·ArrowRight/Left·Home/End 자동 선택, 3px focus-visible ring, `tabpanel` 내용 전환과 54px panel 높이 유지.
- Disabled: `disabled=true`, `tabIndex=-1`, opacity 0.5, 활성화 불가.
- Overflow: 260px frame에서 192px list / 605px scroll width, 이동 버튼 1회 `scrollLeft 0→36`, 선택값 유지, 이전 버튼 활성화.
- Long Label: `nowrap`, `text-overflow: clip`, 단일/복수 긴 라벨에서 실제 필요 폭 기준 overflow. 가시 영역보다 넓은 선택 탭은 시작점을 목록 왼쪽에 맞춘다.
- Responsive: 1280 / 768 / 390px 모두 문서 가로 overflow 없음(`scrollWidth == clientWidth`). 390px에서 기본·fill은 frame 343px, 명시적 narrow specimen은 260px로 유지.
- 2026-10-06 smoke check(ui_viz Storybook 나란히): fill 인접 경계(`margin-left:-1px` + 1px 경계)·선택 탭 8×8 곡선 ±8px 접점·34px·16/24/700·색 유지. Overflow/Fill Overflow/Long Label은 3개 viewport에서 이동 버튼 전환, 선택 탭 시작점 왼쪽 정렬, 페이지 넘침 없음. Click·ArrowRight·Home 동작 확인. 새 회귀 없음.
- 2026-10-08 저장소 소스 검색: Production / Dashboard에는 TabsList 소비처가 없으며 `src/components/tabs.stories.tsx`만 소비한다. 외부 프로젝트는 이 확인 범위에 포함하지 않는다.
## 14. ui-viz 직접 비교 (2026-10-04)
2026-10-06 비교는 외부 ui_viz의 보존된 두 원본 사본의 Tabs JSX/CSS SHA-256 일치를 확인한 뒤 동일 소스와 당시 캡처를 참고했다. 외부 자료는 ZIP에 포함되지 않으며 현재 Source of Truth는 HM 구현·대표 Story와 이 문서다. 당시 백업 Storybook은 dependency가 제거되어 실행하지 못했으므로, 비교 범위는 보존된 소스·캡처와 HM computed value였다.
| 항목 | ui-viz | 수정 전 HM | 판정 / 최종 처리 |
|---|---|---|---|
| 구조·상태 | 자체 controlled Tabs/Tab + roving focus | Radix Root/List/Trigger/Content | `IMPLEMENTATION_CHANGE`; Radix 유지 |
| 기본 geometry·색·type | 34px, 0 16px, 4px, 16/24/700, 연결형 border | computed value 일치 | `MATCHED` |
| hover / selected | inactive만 Black 20%, selected surface 연결 | 일치 | `MATCHED` |
| focus / disabled | 전역 수치 미정 / P0 제외 | 명확한 Radix focus ring / opacity 0.5 | `INTENTIONAL_CHANGE` |
| fill 인접 경계 | 모든 이웃이 `margin-left:-1px` + left border 복원 | active 이웃만 부분 복원 | `REGRESSION`; 원본 겹침 관계 복구 |
| fill 곡선 접점 | active edge에서 정확히 ±8px | edge 밖으로 1px 이격 | `REGRESSION`; ±8px 접점 복구 |
| overflow | 실제 필요 폭, 34px 양끝 control, 한 항목 이동 | 수치·동작 일치 | `MATCHED` |
| 매우 긴 선택 탭 | item이 viewport보다 넓으면 시작점 left align | right align으로 라벨 앞부분이 가려짐 | `REGRESSION`; left align 복구 |
| Long Label 검증 | 실제 사용 specimen 존재 | 전용 Story 없음 | `MISSING`; 숨김 verification Story와 대표 구역 추가 |
| controlled-only API·13px 실험 라벨 | ui-viz Web mapping/미채택 값 | Radix 양방향 API·16px | `OLD_TEST_ONLY`; 미도입 |
### Before / After
| 항목 | Before | After | ui-viz |
|---|---|---|---|
| fill 이웃 경계 | 후속 inactive left border 0 | left border 1px + margin-left -1px | 동일 |
| fill 곡선 | active edge와 1px gap | edge 기준 `-8px` | 동일 |
| viewport보다 넓은 선택 tab | 오른쪽 끝 정렬 | 왼쪽 시작점 정렬 | 왼쪽 시작점 정렬 |
| Long Label Story | 없음 | 단일/복수/260px 검증 | 사용 specimen 존재 |
## 15. 외부 소비자 영향 / 호환성 (2026-10-08)
- **변경 전**: `` → Legacy non-fill (`fill=false`, `panelSurface="surface"`).
- **변경 후**: `` → Primary Fill (`fill=true`, `panelSurface="content"`).
- **이전 스타일 유지**: ``. 이전 White 패널 조합(`bg-card`)도 유지한다. `panelSurface="surface"`를 함께 명시해도 non-fill 시각은 같다.
- **`fill`만 명시한 기존 소비자**: 이전에는 Secondary(surface), 이제는 Primary(content)가 된다. 이전 Secondary를 유지하려면 ``와 surface 패널을 사용한다.
- **`panelSurface`만 명시한 기존 소비자**: 이전에는 non-fill이라 시각적으로 무시했지만 이제 Fill이 적용된다. `"surface"`는 Secondary, `"content"`는 Primary다. non-fill을 유지하려면 `fill={false}`를 추가한다.
- **두 Props 모두 명시**: 기존 조합의 시각은 유지한다. Props 이름·타입·Radix props·export·DOM 구조는 변경하지 않는다. 타입 호환성과 기본 디자인 호환성은 구분한다.
- **레이아웃 차이**: non-fill은 라벨 필요 폭, Fill은 Overflow가 없을 때 컨테이너 전체를 균등 분할한다. 높이 34px은 같으나 탭 폭·공유 경계·선택 연결 곡선·패널색이 달라질 수 있다. 좁은 폭에서는 기존 Overflow 이동 버튼이 나타날 수 있다.
- **CSS Override 영향**: frame의 기본 attribute가 `data-fill="false"` / `data-panel-surface="surface"`에서 `"true"` / `"content"`로 바뀐다. non-fill 조건 selector, list width / flex, active background / border, 곡선 pseudo-element override를 사용하는 소비자는 검토가 필요하다. `className`과 ref는 계속 tablist에 전달된다.
- **확인 범위**: 현재 저장소의 TabsList 사용은 Story만 확인했다. 외부 소비자 전체의 명시적 Props·패널색·레이아웃·CSS Override는 실제 확인하지 못했으므로 위 내용은 영향 가능성과 이행 안내이며 외부 호환성 전체 검증을 뜻하지 않는다.
## 16. Publisher / Developer 조합 안내 (2026-10-09)
사전공유 v0.2의 D-01(중첩 박스 제거)을 기존 공개 API로 표현한다. Component 자체에는 원래 패널 박스가 없으므로 소비 조합만 바꾸며 새 Variant·Token을 만들지 않는다.
### 16.1 표면 선택과 여백 소유권
| 상황 | List | 콘텐츠 배치 |
|---|---|---|
| 기본 흰색 Card 안 | `panelSurface="surface"` | Card / CardContent의 기본 여백을 유지. 각 TabsContent는 `px-0 py-6` |
| Card 없이 content 페이지 위 | 기본 List 또는 `panelSurface="content"` | 기존 Layout의 content 배경·여백 사용. 탭 전체를 새 흰색 박스로 감싸지 않음 |
| 기존 `Card surface="content"` 안 | `panelSurface="content"` | 실제 하단 표면에 맞춤. 흰색 Card 예제를 기계적으로 적용하지 않음 |
| 내부 정보 구분 필요 | 실제 하단 표면에 맞춤 | 기존 Separator / Alert 등 역할이 있는 Component를 조합 |
```tsx
import { Card, CardContent, CardHeader, CardTitle, Tabs, TabsContent, TabsList, TabsTrigger } from "@hm-design/ui"
프로젝트 정보
기본 정보
일정·산출물
기본 정보
일정·산출물
```
- **Do**: 바깥 Card의 배경·border·radius·24px 기본 여백을 유지하고 중복 `rounded-b border bg-background p-6`만 제거한다. 콘텐츠 내부 Input·Textarea·Table의 고유 스타일은 유지한다.
- **Don't**: Input border, Table 행 배경, Alert 표면까지 일괄 제거하지 않는다. `panelSurface`만 바꾸고 실제 상위 배경은 다른 색으로 두지 않는다.
- `py-6`(현재 24px) / `px-0`은 이 탭 조합의 예시 값이다. 모든 Component의 padding이나 제목→설명 간격으로 승격하지 않는다. [Card](card.md#3-기본-규격)의 Header→Content 24px과 [PageHeader](../patterns/page-header.md#3-token-packagesuisrcthemepage-headercss)의 제목→설명 8px·아래 콘텐츠 20px은 별개다.
- CardDescription은 PageHeaderDescription과 동일 규격이 아니다. [CardDescription의 shadcn 호환 범위](card.md#51-carddescription-사용-범위)를 따른다.
### 16.2 Responsive / Accessibility / 상태 확인
- 소비 Layout의 flex/grid 자식인 **Card 자체**와 CardContent·Tabs는 필요 시 `min-w-0`으로 수축을 허용한다. CardContent에만 지정하면 grid 자식 Card의 min-content 폭(내부 Table 최소 폭 + 좌우 padding)이 페이지를 넓힐 수 있다. 긴 라벨은 말줄임으로 대체하지 않고 기존 Tabs 넘침 이동을 사용한다. Table의 기본 최소 폭 520px과 내부 스크롤은 [Table](table.md)을 유지한다.
- `aria-label` 또는 `aria-labelledby`로 tablist를 식별하고 Trigger / Content의 `value`를 일치시킨다. Input·Textarea에는 연결된 Label을 둔다. 입력 상태를 탭 전환 후에도 유지해야 하면 소비자 상위 state로 관리한다(Usage 예제 참고).
- 클릭, ArrowLeft/Right, Home/End, focus-visible, disabled 건너뛰기, 넘침 버튼과 선택 콘텐츠 전환을 확인한다. Disabled trigger 전체 opacity 0.5는 기존 상태 계약이며 아래 그라데이션 alpha와 별개다.
- Empty / Loading / Error 표현이 필요하면 기존 Component / Pattern을 조합한다. 패널 박스 제거는 데이터 조회·오류 처리나 새로운 Tabs 상태 API를 제공하지 않는다.
- desktop / mobile에서 문서 전체 가로 넘침과 내부 Tabs / Table 스크롤을 구분한다. utility 이름만 확인하지 말고 실제 패널 배경 transparent·border 0·padding 24px 0 및 자식 정렬을 브라우저에서 확인한다. 공통 검증은 [개발 적용 가이드](../guidelines/개발-적용-가이드.md#81-탭-조합과-역할별-간격)를 따른다.
### 16.3 D-02: 상단 흰색 80% 실제 반영
사전공유 폴더의 원문은 v0.2 상태를 유지하지만, 사용자가 2026-10-09 후속 요청에서 보류 항목을 실제 구현까지 반영하도록 명시했다. 이 요청에 따라 공통 Theme 전체가 아니라 **Tabs의 Primary content branch만** 아래와 같이 적용했다.
```css
[data-slot="tabs-frame"][data-fill="true"][data-panel-surface="content"] {
--hm-tab-fill-panel: var(--hm-sem-bg-content);
--hm-tab-fill-face: linear-gradient(
to bottom,
color-mix(in srgb, var(--hm-sem-palette-white) 80%, transparent) 0%,
var(--hm-sem-bg-content) 100%
);
}
```
실제 출력 대상은 Fill의 `[data-slot="tabs-trigger"][data-state="active"]`가 소비하는 `background-image`다. 면의 상단 흰색 alpha만 0.8이고 하단 content는 불투명이다. 방향·stop 위치·텍스트·아이콘·경계·Secondary·Legacy·비선택 Hover·Focus·Disabled 상태 규칙을 유지하며 탭 전체 `opacity`는 1이다(기존 Disabled opacity 0.5는 유지). 새 공개 Theme JSON alpha 입력이나 Component API를 추가하지 않았다.
현재 저장소의 TabsList 소비처는 Tabs 대표/verification Story와 PageHeader 대표 Pattern 조합이다. 이 content branch를 사용하는 외부 앱도 패키지 CSS 업데이트 시 새 면을 받지만 외부 코드와 override 전체는 제공되지 않아 별도 확인 대상이다. 같은 Default 흰색을 사용하는 Button·Card·Input·다른 Pattern에는 변경이 전파되지 않는다.
비교 분류·반영 범위와 이번 검증은 [사전공유 통합 보고](../guidelines/publisher-integration-2026-10-09.md)를 따른다. 외부 소비 화면과 참고 예시의 모바일 이슈는 소스가 제공되지 않아 추가 확인 대상이다.