Files
HMDesign/docs/components/tabs.md
T

219 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tabs
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Tabs 고유의 API·Token·상태·결정만 기록한다.
## 기본 사용: Primary (2026-10-08)
앱 진입점에서 `@hm-design/ui/theme.css`를 로드한다. `TabsList`의 기본값은 `fill=true`, `panelSurface="content"`이며 기본 API만으로 Primary가 적용된다.
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@hm-design/ui"
<Tabs defaultValue="overview">
<TabsList aria-label="업무 보기">
<TabsTrigger value="overview">개요</TabsTrigger>
<TabsTrigger value="details">상세</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="border bg-background rounded-b p-4">개요 콘텐츠</TabsContent>
<TabsContent value="details" className="border bg-background rounded-b p-4">상세 콘텐츠</TabsContent>
</Tabs>
```
- **Primary**: `<TabsList>`, `<TabsList fill>`, `<TabsList fill panelSurface="content">`는 같은 디자인이다. 선택 면은 White → `--hm-sem-bg-content` Gradient다.
- **Secondary 명시적 사용**: 위 예제의 List를 `<TabsList fill panelSurface="surface" aria-label="업무 보기">`로 바꾸고 **각** `TabsContent`에 `border bg-card rounded-b p-4`를 사용한다. 선택 면은 기존 White surface다.
- **Legacy non-fill 명시적 사용**: `<TabsList fill={false} aria-label="업무 보기">`와 **각** `TabsContent`의 `border bg-card rounded-b p-4` 조합으로 이전 스타일을 유지한다. `<TabsList fill={false} panelSurface="surface">`도 같은 시각이다. OLD_TEST_ONLY / REMOVE_CANDIDATE 이력은 유지하며 별도 승인 없이 삭제하지 않는다.
`TabsContent`는 패널 배경·테두리를 자동으로 지정하지 않는다. 소비자가 Primary에는 `bg-background`(content), Secondary / Legacy에는 `bg-card`(surface)를 연결한다. `panelSurface`는 List의 선택 면·연결 곡선에만 적용되며 콘텐츠 색을 바꾸지 않는다. Bar와 패널 사이에 gap이나 별도 구분선을 넣지 않는다. 상태·Variant는 §4.1, 외부 소비자 변경 안내는 §15를 따른다.
## 1. 상태
| 항목 | 내용 |
|---|---|
| 구현 상태 | 기본 Primary Fill + 명시적 Secondary Fill + Legacy non-fill + 넘침 이동 버튼(D-090) 구현 (`hm-tab` class 없음) |
| 기준일 | 2026-10-08 (Primary 기본값 승인 반영) |
| 기반 | 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 → bg-content Gradient, top / left / right는 Green Light 3(`--hm-tab-fill-line-color`), bottom은 content 패널색이다.
- **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 `<Tabs value onValueChange><Tab/></Tabs>` 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 메뉴를 추가하지 않는다.
- Story의 콘텐츠 panel은 Primary `TabsContent` + `border bg-background`, Secondary / Legacy `TabsContent` + `border bg-card` 소비자 조합 예시다.
## 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이 아님).
## 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-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)
- **변경 전**: `<TabsList>` → Legacy non-fill (`fill=false`, `panelSurface="surface"`).
- **변경 후**: `<TabsList>` → Primary Fill (`fill=true`, `panelSurface="content"`).
- **이전 스타일 유지**: `<TabsList fill={false}>`. 이전 White 패널 조합(`bg-card`)도 유지한다. `panelSurface="surface"`를 함께 명시해도 non-fill 시각은 같다.
- **`fill`만 명시한 기존 소비자**: 이전에는 Secondary(surface), 이제는 Primary(content)가 된다. 이전 Secondary를 유지하려면 `<TabsList fill panelSurface="surface">`와 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는 실제 확인하지 못했으므로 위 내용은 영향 가능성과 이행 안내이며 외부 호환성 전체 검증을 뜻하지 않는다.