SubTab
진행 중 (SubTab component — work in progress)
- 낱개 하위(sub) 탭. 상위 TabNav(밑줄형) 아래에서 보조 탭으로 쓰는 설정 기반(config-driven) 컴포넌트.
- 호스트가
getTabs/getActive/onSelect만 넘기면 렌더. 선택(active) 탭만 채움/라인으로 강조되고, 비선택은 neutral 라인 테두리. - 모든 색·radius·폰트는
.ds-sub-tab*토큰에서 옴 (마크업에 인라인 스타일·하드코딩 hex·!important없음).
규칙 (강제) · 코딩 에이전트용 — 사람은 접힌 채로 두어도 됩니다
SubTab 강제 규칙. 상세는 아래 본문.
마크업
- 호스트가
x-data="subTab({...})"로 감싸고packages/components/html/sub-tab.html을 include 합니다. 손으로 짓지 마세요. - 활성 클래스는
is-active입니다. ⚠️ TabNav는tab-active로 다릅니다 — 복붙하다 자주 틀립니다. - 그룹 modifier(
ds-sub-tab--sm/--ml·--r4/--r8/--r16·--primary·--line)는.ds-sub-tab컨테이너에만 붙습니다. 아이템(.ds-sub-tab__item)에 붙이지 마세요. - 기본값:
size='md'·radius='8'·color='dark'·fill='solid'.- ⚠️
SubTab.ts의SubTabRadius타입 주석은 아직 "16이 기본"이라고 잘못 적혀 있습니다(파일 헤더와sub-tab.html헤더는 2026-07-27에 정정됨). 실제 기본값은 8(SubTab.ts의opts.radius ?? "8")이에요.
- ⚠️
- 표 위 기간 프리셋 등 「보조 필터」 자리에는
size='sm'+fill='line'을 씁니다 — 날 유틸(px-2 py-1 border rounded-lg)로 비슷한 칩을 새로 만들지 마세요. nav용 서브탭(md·solid)과 한 화면에 놓일 때 위계가 무너집니다. 자세한 건sm은 어디에 쓰나. SubTabItem에badge·updateDot은 없습니다 — 슬롯은leadingIcon/trailingIcon/leadingText/trailingText/disabled뿐이에요.
⚠️ 트랩 (최우선) — 이 버튼에 border-* 유틸을 붙이면 radius 축이 통째로 죽습니다.ds-sub-tab__item은 전역 <button> 4px radius 강제 1번 규칙(design-system.css의 button:not([class*="bg-"]):not(.ds-tab-item):not(.ds-sub-tab__item)…)에서는 제외돼 있습니다 — 자체 radius 3단계 --r4/--r8/--r16를 갖기 때문이고, 그 이유가 규칙 안의 주석에 적혀 있어요. 그런데 2번 규칙 button[class*="border"]:not(.ds-tab-item)에서는 제외돼 있지 않습니다. 지금 안 터지는 유일한 이유는 파셜이 그 버튼에 Tailwind border-* 클래스를 안 붙였기 때문이에요. → 그 버튼에 border·border-2·border-neutral-200 같은 클래스를 하나라도 추가하는 순간 4px !important 강제가 되살아나서 radius:'16' 같은 설정이 전부 무시됩니다. 테두리는 이미 .ds-sub-tab__item 블록이 CSS로 갖고 있어요 — 유틸을 덧붙이지 마세요.
기타
- 복제본 없음 — 앱
main.css어디에도.ds-sub-tab재정의가 없습니다(탭 계열 중 유일하게 깨끗해요). 정본만 고치면 돼요. - 호출처는 admin
leave-management.html(nav 서브탭leaveSubTab+ 기간 프리셋leaveApprovalDatePreset)·documents.html(자격증 기간 프리셋)이고, 전부 URL 라우트가 아니라 페이지 내 상태를 씁니다. 서브탭을 URL 라우트 키로 승격시키면 TabNav의 id 일치·popstate 트랩이 그대로 적용됩니다.
.ds-segment(세그먼트)와 다른 형태예요
세그먼트(.ds-segment, iOS 스타일)는 하나의 회색 통 안에 항목이 붙어 있고 선택 항목만 흰 pill로 이동해요. SubTab은 낱개 탭이 간격을 두고 나열되고, 선택 탭을 검정/주황으로 강조해요 — 상위 탭 아래 "하위 탭" 용도예요.
시각 미리보기
아래는 실제로 클릭되는 라이브 미리보기입니다 — 프로덕션과 동일한
.ds-sub-tab*클래스로 렌더돼요. 상단 라디오(size·radius) + 체크박스(color·fill) 로 4축을 바꿔 보고, 탭을 클릭해 선택(active) 전환도 확인하세요.
선택 탭: 마리떼프랑소와저버 — 탭을 클릭해 전환해 보세요
상위 탭과의 계층 구조
SubTab은 상위 TabNav(밑줄형) 바로 아래에 놓이는 하위 탭이에요. 상위 탭으로 큰 영역을 가르고, 그 안을 SubTab으로 다시 나눠요. 아래는 상위(밑줄) → 하위 를 한 컨테이너에 쌓은 예시예요 — 상위·하위 탭이 동일한 배경색을 공유해요. 둘 다 실제 클래스로 렌더돼요.
여백은 컴포넌트가 아니라 호출처가 정합니다
.ds-sub-tab은 바깥 여백(margin)이 없어요 — 소유한 간격은 탭 사이 gap: 8px 하나뿐입니다. 그래서 위아래 여백은 전부 감싸는 래퍼가 결정하고, "SubTab 기본 여백" 같은 건 존재하지 않아요.
현재 값(2026-07-27 기준):
| 위치 | 위 | 아래 |
|---|---|---|
근태 현황 > 연차 — nav 서브탭 (leave-management.html) | 20px (pt-5) | 20px (아래 패널 첫 블록의 pt-5) |
| 문서 결재 · tm 문서 관리 — 기간 프리셋 | 12px (mt-3, 필터 줄 바로 아래) | 20px (mb-5, 표와의 간격) |
- nav 서브탭 = 위아래 20px, 보조 필터(프리셋) = 위 12px · 아래 20px 이 현재 규약이에요. 새 화면도 이 값을 따르세요.
- 서브탭 바 하나를 여러 패널이 공유하면(연차현황·연차/휴가 결재처럼) 각 패널 첫 블록의
pt를 반드시 같은 값으로 맞춰야 해요 — 다르면 탭을 바꿀 때마다 아래 내용이 미세하게 튑니다.
⚠️ 이 문서는 오랫동안 "상위·하위 탭 사이 간격 24px"이라고 적어 뒀지만 그 값을 쓰는 화면은 하나도 없었습니다(전부 20px 또는 12px). 2026-07-27에 실제 값으로 정정했어요.
┌─ nav.ds-tab-nav (상위 탭, 밑줄형) ──────────────┐
│ 연차 관리 ‾‾‾‾ 외근 관리 │
├──────────────────────────────────────────────┤
│ ( 1 전체 ) ( 2 대기 ) ( 3 완료 ) ← .ds-sub-tab (하위 탭) │
└──────────────────────────────────────────────┘단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/SubTab.ts |
| HTML partial | packages/components/html/sub-tab.html |
| 스타일 | packages/static/styles/design-system.css (.ds-sub-tab*) |
| Alpine 등록 | subTab (plugin.ts) |
토큰 소유권: color·radius·typography·탭 geometry(높이/패딩)는 모두 DS 소유(
.ds-sub-tab*클래스). arbitrary[...]·하드코딩 hex·새!important없음.
구조
┌─ .ds-sub-tab (그룹: inline-flex, gap 8) ───────────────────────┐
│ [leadingText] [leadingIcon] 라벨 [trailingText] [trailingIcon] │
│ └─ .ds-sub-tab__item (각 슬롯은 present일 때만 렌더) ──────────┘ │
│ 선택 탭만 .is-active → 채움/라인 강조, 비선택은 neutral 라인 │
└─────────────────────────────────────────────────────────────────┘- 탭 단위 슬롯 (있으면 켜짐):
leadingIcon/trailingIcon(SVG),leadingText/trailingText(보조 텍스트),disabled. - 이미지의 "1 / 2 / 3" 번호는 자동 넘버링이 아니라 라벨 앞
leadingText(또는 라벨 문자열)로 넣어요.
축(axis) — 4가지
그룹(.ds-sub-tab)에 모디파이어를 조합해요. subTab({ size, radius, color, fill }) 옵션이 그대로 subTabGroupClass로 반영돼요.
1) size — sm / md / ml
| token | min-height | padding | 라벨 폰트 | 클래스 |
|---|---|---|---|---|
sm | 28px | 5px 12px | --text-b6 (12px) | .ds-sub-tab--sm |
md (기본) | 36px | 8px 14px | --text-b5 (13px) | .ds-sub-tab (modifier 없음) |
ml | 44px | 11px 18px | --text-b4 (14px) | .ds-sub-tab--ml |
sm — 28px · 라벨 12px
md — 36px · 라벨 13px (기본)
ml — 44px · 라벨 14px
sm은 어디에 쓰나
보조 필터 자리예요. 대표 사례가 표 위의 기간 프리셋(이번 주 · 지난 1주 · … · 전체)입니다.
핵심은 nav용 서브탭(md · solid)과 한 화면에 같이 놓일 수 있다는 점이에요. 근태 현황 > 연차 > 「연차/휴가 결재」가 그런 화면인데, 위엔 페이지를 가르는 서브탭이 있고 아래엔 기간 프리셋이 있어요. 둘 다 md · solid면 똑같이 생긴 알약 줄이 두 개 놓여서 "누르면 페이지가 바뀌는 것"과 "표만 다시 그리는 것"을 구분할 수 없어요.
그래서 두 축을 함께 씁니다:
| 역할 | 설정 | 읽히는 것 |
|---|---|---|
| nav (페이지 이동) | md · solid | 크고 검정 채움 — "지금 이 페이지" |
| 보조 필터 (기간 프리셋) | sm · line | 작고 테두리만 — "이 필터가 켜짐" |
sm이 없던 동안 각 화면이 이 모양을 날 유틸(px-2 py-1 border rounded-lg)로 손수 만들어 쓰고 있었어요(근태 4개 화면). 새로 만들지 말고 이 단을 쓰세요.
선택 없음(
getActive()가"")도 정상 상태예요. 기간 프리셋은 "아무것도 안 고름"이 기본이고, 사용자가 달력으로 날짜를 직접 지정하면 다시 해제돼요. SubTab은 세그먼트(.ds-segment)와 달리 항상 하나 선택을 강제하지 않으니 그대로 두면 됩니다 — 이 상태를 만들려고 「직접 지정」 같은 항목을 지어내지 마세요.
2) radius — 4 / 8 / 16
시스템 radius 토큰 3단계로 모서리를 바꿔요(기본 8. 16은 첨부 이미지처럼 더 둥근 형태).
| 값 | 토큰 | 클래스 |
|---|---|---|
4 | --radius-4 | .ds-sub-tab--r4 |
8 (기본) | --radius-8 | .ds-sub-tab--r8 |
16 | --radius-16 | .ds-sub-tab--r16 |
radius 4
radius 8 (기본)
radius 16
3) color × 4) fill — 선택 강조 4종
선택(active) 탭의 강조를 color(dark / primary) × fill(solid / line) 로 조합해요. 비선택은 4종 모두 공통 neutral 라인 테두리예요.
| color · fill | 선택(active) | 클래스 |
|---|---|---|
dark · solid (기본) | neutral-900 채움 + 흰 글자 | (modifier 없음) |
dark · line | neutral-900 테두리 + neutral-900 글자 | .ds-sub-tab--line |
primary · solid | --ds-color-primary(주황) 채움 + 흰 글자 | .ds-sub-tab--primary |
primary · line | 주황 테두리 + 주황 글자 | .ds-sub-tab--primary.ds-sub-tab--line |
dark · solid (기본)
dark · line
primary · solid
primary · line
호출 패턴
<div x-data="subTab({
getTabs: () => mySubTabs, // () => SubTabItem[]
getActive: () => activeSubId, // () => string
onSelect: (id) => switchSub(id), // (id) => void
size: 'md', // 'sm' | 'md'(기본) | 'ml' — 선택
radius: '8', // '4' | '8'(기본) | '16' — 선택
color: 'dark', // 'dark'(기본) | 'primary' — 선택
fill: 'solid', // 'solid'(기본) | 'line' — 선택
})">
<!-- include "../../packages/components/html/sub-tab.html" -->
</div>→ partial 한 줄로 .ds-sub-tab 그룹 전체가 렌더됨. 탭 목록·선택 ID는 호스트 상태에 반응(reactive).
API
subTab(opts: SubTabOptions) Alpine.data factory.
SubTabOptions (모두 optional)
| Prop | 타입 | 설명 |
|---|---|---|
getTabs | () => SubTabItem[] | 반응형 탭 목록 source |
getActive | () => string | 반응형 선택 탭 id source |
onSelect | (id: string) => void | 비활성 아닌 탭 클릭 시 id로 호출 |
size | 'sm' | 'md' | 'ml' | 탭 크기. 생략 시 md(36px); sm은 보조 필터용 작은 탭(28px), ml은 큰 탭(44px) |
radius | '4' | '8' | '16' | 모서리 단계. 생략 시 8 |
color | 'dark' | 'primary' | 선택 강조색. 생략 시 dark |
fill | 'solid' | 'line' | 선택 채움 방식. 생략 시 solid |
SubTabItem (탭 1개)
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
id | string | ✅ | onSelect에 넘어가는 고유 id |
label | string | ✅ | 메인 탭 텍스트 |
leadingIcon | string | 앞 SVG 아이콘 이름(레지스트리). 생략 시 숨김 | |
trailingIcon | string | 뒤 SVG 아이콘 이름. 생략 시 숨김 | |
leadingText | string | 라벨 앞 보조 텍스트(예: 번호). 생략 시 숨김 | |
trailingText | string | 라벨 뒤 보조 텍스트. 생략 시 숨김 | |
disabled | boolean | 클릭 무시 + 흐리게 |
사용된 토큰
| 카테고리 | 토큰 |
|---|---|
| Color (비선택) | --color-neutral-200(테두리) · --color-neutral-500(글자) · --color-neutral-300/700·--color-neutral-50(hover) |
| Color (선택) | --color-neutral-900(dark) · --ds-color-primary(primary) · --ds-bw-white(solid 글자) |
| Color (보조 텍스트) | --color-neutral-400 |
| Typography | --text-b6(12px, sm) · --text-b5(13px, md) · --text-b4(14px, ml) · --text-b6(12px, 보조 텍스트) |
| Radius | --radius-4 / --radius-8 / --radius-16 |
검토 필요 사항
- ✅ radius 중간값 = 8 (기본값) — 현황 유지 확정(2026-07-13). 원래 디자인 요청은
12였고 당시엔 12px 토큰이 없어 가장 인접한--radius-8을 채택했는데,--radius-12가 신설되어(모서리) 그 제약은 사라졌습니다. 그럼에도 12px 상향은 시각 변경이라 현행 8px를 유지하기로 결정했습니다 — 이 항목은 닫혔습니다(다시 열려면 디자인 재검토가 선행). - 🟢 탭 높이 28/36/44 — 상위 TabNav(40/48)보다 한 단계 작게 잡아 "상위 > 하위" 위계를 시각적으로 분리.
sm(28)은 그보다 또 한 단 아래로, nav 서브탭(md)과 같은 화면에 놓이는 보조 필터용.
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-07-27 | size='sm'(28px / --text-b6 12px) 추가 — 표 위 기간 프리셋 같은 「보조 필터」 자리를 위한 단. nav용 서브탭(md·solid)과 한 화면에 놓일 때 sm·line으로 위계를 가른다. 이 단이 없어 근태 4개 화면이 날 유틸로 같은 칩을 손수 만들어 쓰던 것을 정본으로 회수. <DemoSubTab> size 컨트롤이 체크박스(ml on/off) → 라디오(sm/md/ml)로 바뀜 |
| 2026-06-30 | SubTab 컴포넌트 신규 — 낱개 하위 탭(.ds-sub-tab* + subTab + sub-tab.html). 축 4종(size sm/md/ml · radius 4/8/16 · color dark/primary · fill solid/line), 비선택 공통 neutral 라인. <DemoSubTab> 라이브 미리보기 + 상위 TabNav와의 계층 데모 |