Bottom Navigation
폰 화면 아래에 떠 있는 탭 바(하단 탭 바) — Desk 앱 · Desk 웹 768 미만의 주 메뉴다. 칸은 다섯을 넘지 않고 어느 화면에서나 같다(Desk 는 홈 · 가계부 · + · 캘린더 · 전체 — 아래 "Desk 에서는"). 가운데 + 는 탭이 아니라 그 화면의 추가 동작이다. 다른 이동 부품(Top Navigation · Side Navigation · Side Panel · Tabs)과 고르는 표는 DESIGN.md Layout 의 "이동 부품 고르기" 한 곳이다.
규칙은 당근 SEED Bottom Navigation(Apache-2.0)을 따른다 — "앱의 루트 페이지 하단에 고정되어 있는 네비게이션 바로, 다섯 개의 상위 탭 간 이동을 제공합니다." 5칸을 넘지 않고, 라벨은 한글 5자 이내 · 글자 크기 설정을 따르지 않으며, 칸은 최대 480 안에 모으고, 알림 배지는 2칸까지 · "99+", 더 나눌 구분은 화면 위 Tabs 로 둔다. SEED 는 디자인 문서 · 그림뿐이라(rootage · 코드 없음) 바의 모양은 groo 가 지켜 온 떠 있는 알약을 고쳐 쓴다 — 바탕을 불투명하게 · 줄어들 때도 칸마다 이름 · 줄어든 바를 누르면 펴짐(2026-10-04 사용자 결정 4B). 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다. groo 에 처음 두는 스펙이다 — 지금 Desk 웹 · 앱의 탭 바(tabbar.tsx · p_tab_bar.dart)를 정리한다.
수치 원본은 bottom-navigation.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
지금 탭 · 크기(펼침 · 줄어듦) · 알림 점 · 화면(홈의 "거래 추가" · 캘린더의 "일정 추가") · 안전 영역(홈 표시줄 있음 · 없음)을 고르면 스펙대로 그린 바와 그 코드가 바뀐다. 목록을 아래로 스크롤하면 바가 줄어들고, 위로 올리거나 바를 누르면 펴진다.
import { CalendarDays, ClipboardList, House, Menu } from "lucide-react"
import { BottomNavigation, BottomNavigationAddButton, BottomNavigationItem } from "@/components/ui/bottom-navigation"
import { SnackbarAvoidOverlap } from "@/components/ui/snackbar"
<SnackbarAvoidOverlap>
<BottomNavigation>
<BottomNavigationItem href="/desk" icon={<House />} label="홈" current={tab === "home"} />
<BottomNavigationItem href="/desk/ledger" icon={<ClipboardList />} label="가계부" current={tab === "ledger"} />
{/* 탭이 아니라 이 화면의 추가 — 이름은 화면마다 */}
<BottomNavigationAddButton aria-label={tab === "calendar" ? "일정 추가" : "거래 추가"} onClick={openAdd} />
<BottomNavigationItem href="/desk/calendar" icon={<CalendarDays />} label="캘린더" current={tab === "calendar"} />
<BottomNavigationItem href="/desk/more" icon={<Menu />} label="전체" current={tab === "more"} />
</BottomNavigation>
</SnackbarAvoidOverlap>
{/* 마지막 줄이 바 위 24 에서 끝난다 — 바가 줄어도 그대로 */}
<main id="main" className="overflow-y-auto" style={{ paddingBottom: BOTTOM_NAVIGATION_INSET }}>…</main>Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Root | 바 — 떠 있는 알약. 칸 다섯을 똑같이 나누고 넓은 화면에서는 480 으로 가운데에 선다. |
| ⓑ Item | 탭 칸 — 아이콘 + 라벨. 누르면 그 탭으로. |
| ⓒ Label | 라벨 — 11 / 15, 한글 5자 이내. 줄어든 바에서는 보이지 않지만 이름으로 남는다. |
| ⓓ Add Button | 가운데 + — 브랜드 원 44. 그 화면의 추가(거래 추가 · 일정 추가). |
| ⓔ Notification | 알림 — 탭 아이콘 오른쪽 위(Notification Badge). 2칸까지. |
부위
| 부위 | 설명 |
|---|---|
root | 바 — 화면 아래에 떠 있는 알약. 칸 다섯을 똑같이 나눈다 |
item | 탭 칸 — 아이콘 + 라벨. 누르면 그 탭으로(지금 탭이면 맨 위 + 그 탭의 첫 화면) |
icon | 탭 아이콘 — lucide 선 아이콘 |
label | 탭 라벨 — 한글 5자 이내 · 글자 크기 설정을 따르지 않는다 |
addButton | 가운데 + — 브랜드 원. 그 화면의 추가(거래 추가 · 일정 추가) — 탭이 아니다 |
addIcon | + 아이콘 — 흰색 |
notification | 탭의 알림 — Notification Badge(점 · 숫자). 2칸까지 |
bodyInset | 본문 아래 여백 — 셸이 본문 스크롤에 준다. 마지막 줄이 바 위로 올라온다 |
focusRing | 키보드 포커스 링(웹) |
Properties
바 — 펼침 · 줄어듦
펼친 바는 높이 66 · 화면 끝에서 좌우 14 · 아래 14(홈 표시줄이 있으면 안전 영역 − 6 = 28)이다. 아래로 스크롤하면 줄어든다 — 높이 48 · 좌우 36 · 라벨은 보이지 않게(이름은 남는다) · + 는 36. 바탕은 불투명한 떠 있는 표면(bg-layer-floating)이고 흐림(blur)을 두지 않는다 — 뒤로 짙은 카드 · 브랜드 카드가 지나가도 라벨 대비가 그대로다. 그림자는 shadow-s3, 안쪽 1px stroke-neutral-subtle 로 흰 화면 위에서도 가장자리가 잡힌다.
크기
| size | 높이 | 상하 여백 | 좌우 여백 | 아래 바깥 여백 | 좌우 바깥 여백 | label 보이기 | addButton 크기 | addIcon 크기 | 설명 |
|---|---|---|---|---|---|---|---|---|---|
regular (기본) | 66px | spacing-x1_5 6px | spacing-x2_5 10px | max(14px, 아래 안전 영역 − 6px) — 홈 표시줄이 있으면 34 − 6 = 28 — 바의 아래 여백 6 만 안전 영역에 걸치고 누르는 칸은 안전 영역 위에서 끝난다 | spacing-x3_5 14px — 화면 끝 → 바 14(좌우 안전 영역을 더한다) | — | 44px | 24px | 펼침 66 — 기본 · 맨 위 근처 · 위로 스크롤할 때 |
compact | 48px | 0px — 칸이 바 높이 48 을 다 채운다 — 누르는 영역 44 이상(v106). 보이는 모양은 그대로 | spacing-x2 8px | max(12px, 아래 안전 영역 − 8px) — 홈 표시줄이 있으면 26 — 칸(48)은 안전 영역 위에서 끝난다 | spacing-x9 36px — 화면 끝 → 바 36 | 보이지 않게(sr-only) — 보이는 라벨만 숨긴다 — 글은 남아 칸의 이름이 된다(앱 Semantics label 그대로) | 36px — 원 36 | 20px — v106 단계 — 지금 18 을 20 으로 | 줄어듦 48 — 아래로 스크롤할 때. 라벨은 숨기고 칸마다 이름은 남긴다 |
바
| 속성 | 값 | 비고 |
|---|---|---|
| 최대 너비 | 480px | 넓은 화면에서는 480 으로 가운데에 선다(SEED — 칸이 지나치게 퍼지지 않게) |
| 모서리 | radius-full 9999px | |
| 배경 | color-bg-layer-floating #FFFFFF · 다크 #2D3346 | 불투명 — 흐림(backdrop-blur)을 걷었다(결정 4). 뒤로 짙은 카드가 지나가도 라벨 대비가 그대로다 |
| 그림자 | shadow-s3 · 다크 shadow-s3-dark | 떠 있는 높이 — 지금 손으로 쓴 0 10 30 18% + 0 2 8 8% 의 가장 가까운 토큰(Elevation Local 2 · 떠 있는 동작) |
| 테두리 | color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D | 안쪽 1px — 흰 바가 흰 화면 위에서 가장자리를 갖는다(지금 border-subtle 70% 의 불투명 짝) |
| 테두리 두께 | 1px | |
| 칸 수 | 5 | 탭 넷 + 가운데 + — 칸 폭은 똑같이 나눈다. 5칸을 넘지 않는다 |
| 간격 | 2px | |
| z-index | 50 | z-sticky — specs/z-index.md L1. 스낵바(L6)가 위에 뜬다 |
| 전환 대상 | height, margin, padding | 펼침 ↔ 줄어듦 |
| 전환 시간 | motion-duration-d4 200ms | |
| 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) |
칸 · 아이콘 · 라벨
칸은 바 폭을 똑같이 나눈다. 바 66 · + 원 44(줄어든 바 36) · 아이콘 24 는 px 다 — 글자 크기와 상관없이 같다(03-G 1A). 아이콘은 lucide 선 아이콘 24, 라벨은 11 / 15 · 500 이고 글자 크기 설정을 따르지 않는다(SEED — 커지면 칸 밖으로 넘친다). 라벨은 한 줄, 한글 5자 이내로 쓴다.
칸
| 속성 | 값 | 비고 |
|---|---|---|
| 쌓는 방향 | 세로 | 아이콘 위 · 라벨 아래, 가운데 |
| 간격 | 2px | |
| 커서 | pointer | |
| 터치 영역 | 칸 전체 | 펼침 약 61 × 54 · 줄어듦 약 53 × 48(360 폭) — AA ✓ · AAA ✓(줄어든 바는 칸이 바 높이를 다 채운다) |
| tabMemory | 마지막 화면 · 스크롤 | 다른 탭을 다녀오면 그 탭의 마지막 화면 · 스크롤로(가계부 칸은 마지막에 보던 통계로) — 웹 · 앱 같다. 웹은 목록이 그려진 뒤 돌려놓고 못 하면 맨 위(2026-10-16 사용자 결정 Q-018-4 A). 지금 탭을 다시 누르면 첫 화면 · 맨 위 |
아이콘
| 속성 | 값 | 비고 |
|---|---|---|
| 아이콘 | 24px | lucide 선 아이콘(v106 단계 — 지금 22 를 24 로) |
| 색 | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | |
| 선 굵기 | 2px | lucide 기본 |
라벨
| 속성 | 값 | 비고 |
|---|---|---|
| 글자 | text-t1-static 11px / 15px | 11 / 15 — 글자 크기 설정을 따르지 않는다(웹 text-t1-static, 앱 TextScaler.noScaling — SEED). 지금 10.5 를 11 로 |
| 굵기 | 500 | 고르든 안 고르든 500 — 고를 때 폭이 흔들리지 않는다 |
| 글자·아이콘 | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | |
| 줄 수 | 1 | 한 줄 — 라벨은 한글 5자 이내로 쓴다 |
지금 탭
지금 탭은 아이콘 · 라벨이 짙은 글자색(fg-neutral)이고 아이콘 선이 2.5 다. 다른 탭은 fg-neutral-subtle · 선 2 다. 브랜드 색으로 칠하지 않는다 — Tabs · Chip 의 "고름 = 짙은 색" 과 같고, 브랜드 색은 가운데 + 하나에만 남아 더 잘 보인다. 굵기 · 크기는 바꾸지 않는다.
고름
| selected | icon 색 | icon 선 굵기 | label 글자·아이콘 | 설명 |
|---|---|---|---|---|
unselected (기본) | — | — | — | 다른 탭 |
selected | color-fg-neutral #1A1F2E · 다크 #F5F6FA — 짙은 글자색 — 브랜드 색이 아니다(결정 5A · Tabs 와 같은 '고름 = 짙은 색') | 2.5px — v106 — 켜짐 · 선택은 선 2.5 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | 지금 탭 |
가운데 +
가운데 칸은 탭이 아니라 그 화면의 추가 동작이다 — 브랜드 원 44(bg-brand-solid) + 흰 + 24, 라벨은 없다. 이름은 화면마다 다르다 — 홈 · 가계부 · 전체에서는 "거래 추가", 캘린더에서는 "일정 추가". 누르면 그 추가(시트)를 연다. 줄어든 바에서는 원 36 · + 20 이다.
가운데 +
| 속성 | 값 | 비고 |
|---|---|---|
| 크기 | 44px | 원 — 칸 가운데. 누르는 영역은 칸 전체 |
| 모서리 | radius-full 9999px | |
| 배경 | color-bg-brand-solid HR #075B3C · Desk #0147AD | 브랜드 채움 — 두 모드 같다. 탭 바에서 브랜드 색은 + 하나뿐이다 |
| 그림자 | none |
본문 아래 여백
바가 본문을 덮으므로 본문 스크롤의 맨 아래에 여백을 둔다 — 바의 아래 자리 + 66 + 24. 끝까지 내리면 마지막 줄이 바 위 24 에서 끝난다. 바가 줄어도 여백은 그대로 둔다(줄 때마다 본문이 밀리지 않게). Scroll Fog 는 탭 바 위 목록에 걸지 않는다.
본문 아래 여백
| 속성 | 값 | 비고 |
|---|---|---|
| 아래 여백 | 바의 아래 자리 + 66 + 24 | 마지막 줄이 바 위 24 에서 끝난다 — 안전 영역이 없으면 14 + 66 + 24 = 104, 34 면 28 + 66 + 24 = 118(지금 값). 줄어도 그대로 둔다 |
State
| 상태 | 모습 |
|---|---|
enabled | 바탕 없음 — 마우스 호버 모양도 없다 |
pressed | 탭 칸은 2px 거리 축소만 — 색은 그대로(손을 떼기 전에 고른 것처럼 보이지 않게). + 는 bg-brand-solid-pressed + 축소 |
focused | 웹 — 키보드 포커스에만 링. 칸은 안쪽 2px, + 는 원 바깥 2px |
상태
| 상태 | item 배율 | item 축소 시간 | item 축소 곡선 | addButton 배경 | addButton 그림자 | addButton 배율 | 링 두께 | 링 간격 | 링 색 | focusRing 모서리 |
|---|---|---|---|---|---|---|---|---|---|---|
enabled 기본 — 마우스 호버 모양은 없다(Tabs 와 같다) | — | — | — | color-bg-brand-solid HR #075B3C · Desk #0147AD — 브랜드 채움 — 두 모드 같다. 탭 바에서 브랜드 색은 + 하나뿐이다 | none | — | — | — | — | — |
pressed 누름 — 탭 칸은 2px 거리 축소만(색은 그대로), +는 누름 색 + 축소 | 칸 2px 거리 축소 — 색은 그대로 — 손을 떼기 전에 고른 것처럼 보이지 않게(v104 · Tabs). 모션 줄이기면 하지 않는다 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | color-bg-brand-solid-pressed HR #064F33 · Desk #013D96 | none | 2px 거리 축소 — 기준은 원 지름 — 펼침 44 → 0.955 · 줄어듦 36 → 0.944(v104) | — | — | — | — |
focused 웹 — 키보드 포커스에만 링 | — | — | — | color-bg-brand-solid HR #075B3C · Desk #0147AD — 브랜드 채움 — 두 모드 같다. 탭 바에서 브랜드 색은 + 하나뿐이다 | none | — | 2px | -2px — 칸 안쪽 — 붙은 이웃 칸 · 바 가장자리에 걸리지 않는다. + 는 원 바깥 2 | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA | radius-r3 12px |
모션
| 전환 | 시간 | 곡선 | 대상 | 비고 |
|---|---|---|---|---|
펼침 ↔ 줄어듦 | motion-duration-d4 200ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | height, margin, padding | 라벨은 줄어들기 시작할 때 사라지고 다 펴진 뒤 나타난다. 모션 줄이기면 바로 바뀐다 |
누름 — 칸 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | scale | 2px 거리 축소 — 모션 줄이기면 하지 않는다 |
누름 — + | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | background-color, scale |
Guidelines
다섯 칸 — 어느 화면에서나 같다
칸은 다섯을 넘지 않고, 어느 화면에서도 칸이 바뀌지 않는다 — 다른 탭으로 늘 한 번에 간다. 더 나눌 구분은 탭 바 칸을 늘리지 않고 그 탭 화면 위의 Tabs(Line)로 둔다(SEED — "더 많은 구분이 필요하다면 화면 상단에 Tabs 를 사용"). 탭 바 칸을 화면마다 바꾸거나(← · 4칸) 6칸 이상으로 늘리지 않는다.
Desk 에서는
아래는 부품 규칙이 아니라 Desk 의 화면 배치다(2026-10-22 사용자 결정 9B — https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L). 탭 바는 홈 · 가계부 · + · 캘린더 · 전체 다섯이다. 가계부 · 자산 · 통계 · 예산은 탭 바 칸이 아니라 가계부 화면 위의 Tabs(Line)다 — 웹은 고른 탭을 주소에 둔다.
줄어들기 — 이름은 남고, 누르면 펴진다
아래로 20 이상 스크롤하면 바가 줄어들고, 위로 28 이상 스크롤하거나 맨 위 40 안으로 오면 펴진다(스크롤 방향이 바뀌면 다시 센다). 줄어든 바에서도 칸마다 이름이 남아 보조 기술이 읽는다. 줄어든 바를 누르면 펴진다 — 칸을 누르면 그 탭으로 가면서 바도 펴진다(앱도 같다). 가로로 스크롤하는 줄 · 시트 안의 스크롤은 세지 않는다. 폰 원장 머리가 스크롤에 숨는 것(Scroll Auto Hide, 2026-10-10)과는 따로다 — 바는 이 규칙대로 줄고 펴진다.
탭을 옮길 때 — 탭마다 기억
탭마다 마지막 화면과 스크롤 위치를 기억한다 — 홈을 600 내려 두고 캘린더를 다녀와도 홈은 600 이고, 가계부 칸은 마지막에 보던 통계로 돌아온다. 웹도 앱과 같다 — 웹은 돌아온 화면의 목록이 그려진 뒤 스크롤을 돌려놓고, 돌려놓지 못하면(그 자리까지 내용이 없으면) 맨 위에서 시작한다(2026-10-16 사용자 결정 — 구현 세션 질문 Q-018-4 A). 지금 탭을 다시 누르면 그 탭의 첫 화면으로 가고 맨 위로 올라간다(같은 주소를 쌓지 않는다 — 지금 주소를 고쳐 쓴다). 다른 화면으로 들어갈 때의 초점 · 문서 제목은 Top Navigation 의 "화면을 옮길 때" 다.
| 누르는 것 | 동작 |
|---|---|
| 다른 탭 | 그 탭의 마지막 화면 · 스크롤 위치로(웹은 목록이 그려진 뒤) |
| 지금 탭(첫 화면) | 맨 위로 |
| 지금 탭(그 아래 화면) | 그 탭의 첫 화면으로 · 맨 위 — 주소를 고쳐 쓴다 |
앱 기기 뒤로 — 홈이 아닌 탭 첫 화면(가계부 묶음 포함)에서는 홈으로, 홈에서는 앱을 닫는다. 탭을 지나온 순서는 쌓지 않는다(2026-09-12 사용자 결정 — 2026-10-22 결정 7B 로 스펙에 적었다, https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L). 그 아래 화면에서는 Top Navigation 의 ← 와 같은 곳이다. 웹은 브라우저 기록대로다(정하지 않았다).
가운데 + 는 화면의 추가
- 는 그 화면의 추가 하나다 — 홈 · 가계부 · 전체는 거래 추가, 캘린더는 일정 추가. 이름이 화면마다 바뀌므로 버튼 이름도 같이 바꾼다(앱도 이름을 단다). + 를 탭 칸처럼 라벨을 단 칸으로 두거나, 탭 바에서 빼서 떠 있는 버튼으로 옮기지 않는다. 탭 바가 있는 화면에는 Floating Action Button 을 두지 않는다 — 추가는 + 가 맡는다.
라벨 — 짧게, 이름만
라벨은 한글 5자 이내의 명사다(SEED). 개수 · 새 소식은 라벨에 붙이지 않고 알림 배지로 — 배지는 2칸까지만 단다(SEED "3개 이상의 탭에 Badge 가 표시되지 않도록"). 지금 탭 바에 배지를 다는 칸은 없다.
다른 것과 함께
- 스낵바 — 탭 바 위 8 에 뜬다(Snackbar — 탭 바를
SnackbarAvoidOverlap으로 감싼다). - 바닥 고정 버튼 · 입력 화면 — 탭 바가 있는 화면에 바닥 고정 버튼을 두지 않는다 — 탭 바가 바닥을 덮는다. 탭 바가 없는 화면(할 일 · 더치페이)은 바닥 버튼 · Floating Action Button 을 둘 수 있다. 입력은 시트로 띄우고(시트 · 딤이 탭 바를 덮는다), 화면 키보드가 열리면 탭 바를 가린다.
- 안전 영역 — 바의 아래 여백 6 만 홈 표시줄 영역에 걸치고 누르는 칸은 그 위에서 끝난다(Layout 의 Safe Area).
코드
레시피 recipes/shadcn/components/ui/bottom-navigation.tsx 를 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.
BottomNavigation— 바(<nav aria-label="주 메뉴">).compact·defaultCompact·onCompactChange(손으로 다룰 때),scrollRoot(줄어들기를 셀 스크롤 상자 — 기본은 셸 안의 세로 스크롤 모두). 줄어든 바를 누르면 펴진다.BottomNavigationItem— 탭 칸(<a>).href(그 탭의 첫 화면) ·resumeHref(그 탭의 마지막 화면 — 셸이 탭마다 기억한 주소. 다른 탭에서 누르면 여기로, 없으면href) ·icon(lucide 아이콘) ·label(5자 이내) ·current(지금 탭 —aria-current="page") ·notification(점 — 이름에 "새 소식" 을 덧붙인다) ·onReselect(지금 탭을 다시 눌렀을 때 — 기본은 그 탭의 첫 화면으로 주소를 고쳐 쓰고 맨 위로).BottomNavigationAddButton— 가운데 +(<button type="button">).aria-label필수("거래 추가" · "일정 추가").BOTTOM_NAVIGATION_INSET— 본문 아래 여백 CSS 식(calc(max(14px, env(safe-area-inset-bottom) - 6px) + 90px)). 셸이 본문 스크롤의padding-bottom에 쓴다.
앱은 같은 값을 Dart 로 둔다(PTabBar — 칸은 Semantics(button: true, selected: current, label), + 도 이름을 단다).
Desk 셸 — 다섯 칸
import { CalendarDays, ClipboardList, House, Menu } from "lucide-react"
import { BottomNavigation, BottomNavigationAddButton, BottomNavigationItem } from "@/components/ui/bottom-navigation"
import { SnackbarAvoidOverlap } from "@/components/ui/snackbar"
<SnackbarAvoidOverlap>
<BottomNavigation>
{/* 다른 탭에서 누르면 그 탭의 마지막 화면(resumeHref — 셸이 탭마다 기억), 지금 탭을 다시 누르면 첫 화면(href) · 맨 위 */}
<BottomNavigationItem href="/desk" resumeHref={lastPath.home} icon={<House />} label="홈" current={tab === "home"} />
<BottomNavigationItem href="/desk/ledger" resumeHref={lastPath.ledger} icon={<ClipboardList />} label="가계부" current={tab === "ledger"} />
{/* 탭이 아니라 이 화면의 추가 — 이름은 화면마다 */}
<BottomNavigationAddButton aria-label={tab === "calendar" ? "일정 추가" : "거래 추가"} onClick={openAdd} />
<BottomNavigationItem href="/desk/calendar" resumeHref={lastPath.calendar} icon={<CalendarDays />} label="캘린더" current={tab === "calendar"} />
<BottomNavigationItem href="/desk/more" resumeHref={lastPath.more} icon={<Menu />} label="전체" current={tab === "more"} />
</BottomNavigation>
</SnackbarAvoidOverlap>본문 아래 여백
import { BOTTOM_NAVIGATION_INSET } from "@/components/ui/bottom-navigation"
{/* 마지막 줄이 바 위 24 에서 끝난다 — 바가 줄어도 그대로 */}
<main id="main" className="overflow-y-auto" style={{ paddingBottom: BOTTOM_NAVIGATION_INSET }}>…</main>Behavior
| 인터랙션 | 동작 |
|---|---|
| 탭 칸 누르기 | 그 탭의 마지막 화면 · 스크롤 위치로. 지금 탭이면 첫 화면 · 맨 위로(주소를 고쳐 쓴다) |
| + 누르기 | 그 화면의 추가(거래 · 일정)를 연다 |
| 아래로 스크롤(20 이상) | 바가 48 로 줄어든다 — 라벨은 보이지 않고 이름은 남는다 |
| 위로 스크롤(28 이상) · 맨 위 40 안 | 펴진다 |
| 줄어든 바 누르기 | 펴진다 — 칸을 눌렀으면 그 칸의 일도 한다(다른 탭이면 그 탭으로, 지금 탭이면 첫 화면 · 맨 위로 — 2026-10-15 구현 세션 질문 Q-014 정리, 웹 · 앱 이미 그렇다) |
Tab | 홈 → 가계부 → + → 캘린더 → 전체. Enter 로 이동 · 추가 |
| 화면 키보드가 열림 | 탭 바를 가린다 |
| 모션 줄이기 | 줄어들기 · 펴지기가 바로 바뀐다 · 누름 축소 없음 |
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1) | 지금 탭 라벨 fg-neutral 바(bg-layer-floating) 위 16.41 · 다크 11.62, 다른 탭 fg-neutral-subtle 5.50 · 5.27 ✓ — 바탕이 불투명해 뒤로 지나가는 내용과 상관없다(지금은 82% 바탕이라 짙은 카드 위에서 3.82 · 줄면 2.74) |
| WCAG 1.4.11 Non-text contrast(≥ 3:1) | 아이콘 16.41 · 11.62 / 5.50 · 5.27 ✓. + 의 흰 아이콘 bg-brand-solid 위 Desk 8.38 · 8.36 · HR 8.17 · 8.36 ✓ — 원과 바(다크 Desk · HR 1.50)는 흰 + 가 알린다. 키보드 포커스 링 Desk 8.38 · 5.28 · HR 8.17 · 5.26 ✓ |
| WCAG 1.4.1 Use of color | 지금 탭은 색(2.99 · 2.20)만이 아니라 선 굵기(2.5 · 2)로도 다르다. 보조 기술에는 aria-current="page" |
| WCAG 1.4.4 Resize text | 라벨은 글자 크기 설정을 따르지 않는다(SEED) — 칸 밖으로 넘치지 않게. 탭 이름은 화면의 다른 자리(Top Navigation 제목)에서 커진다 ⚠ |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 칸 약 61 × 54 · 줄어듦 약 53 × 48 · + 44 ✓ |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | 펼침 칸 54 ✓ · 줄어듦 칸 48 ✓ — 줄어든 바는 칸이 바 높이를 다 채운다(위아래 안쪽 여백 0) · + 44 ✓ |
| WCAG 4.1.2 Name, Role, Value | 줄어든 바의 칸도 이름이 있다(라벨이 보이지 않게만 남는다). + 이름은 화면마다("거래 추가" · "일정 추가") ✓ |
| ARIA | 바 <nav aria-label="주 메뉴"> · 칸은 링크 + 지금 탭 aria-current="page", 아이콘은 aria-hidden. + 는 <button type="button">. 알림 점은 aria-hidden 이고 칸 이름 뒤에 "새 소식". 앱은 바 Semantics(container: true, label) — 이름 "주 메뉴" 는 번역 문구(navMainMenu · en "Main menu", 코드에 한국어 상수를 두지 않는다 — 2026-10-16 사용자 결정 틀 앱 02-003) · 칸 Semantics(button: true, selected, label). 칸 뒤의 "새 소식" 도 번역 문구다 |
Do / Don't
✅ Do
- 다섯 칸을 어느 화면에서나 그대로 — 구분이 더 필요하면 화면 위 Tabs.
- 지금 탭은 짙은 색 + 선 2.5, 브랜드 색은 + 에만.
-
- 의 이름을 화면마다 단다(웹 · 앱).
- 줄어도 이름을 남기고, 누르면 편다.
- 탭마다 마지막 화면 · 스크롤을 기억하고(웹도), 지금 탭을 다시 누르면 맨 위로.
❌ Don't
- 화면 아래에 붙은 바 · 흐린 반투명 바.
- 지금 탭을 브랜드 색으로.
-
- 를 라벨 붙은 칸으로 · 떠 있는 버튼으로 빼기.
- 화면마다 칸을 바꾸기(← · 4칸) · 6칸 이상.
- 지금 탭을 다시 누를 때마다 같은 주소를 쌓기.
- 웹에서만 탭을 옮길 때마다 첫 화면 맨 위로 — 앱과 같게 기억한다.
- 탭 바 위 목록에 끝 흐림(Scroll Fog).
Specification
bottom-navigation.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Bottom Navigation 을 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 최대 너비maxWidth | 480px넓은 화면에서는 480 으로 가운데에 선다(SEED — 칸이 지나치게 퍼지지 않게) |
| 간격gap | 2px | ||
| 칸 수columns | 5탭 넷 + 가운데 + — 칸 폭은 똑같이 나눈다. 5칸을 넘지 않는다 | ||
| 모서리radius | radius-full9999px(알약) | ||
| 배경background | bg-layer-floating#FFFFFF다크 #2D3346불투명 — 흐림(backdrop-blur)을 걷었다(결정 4). 뒤로 짙은 카드가 지나가도 라벨 대비가 그대로다 | ||
| 테두리borderColor | stroke-neutral-subtle#EDEFF3다크 #353B4D안쪽 1px — 흰 바가 흰 화면 위에서 가장자리를 갖는다(지금 border-subtle 70% 의 불투명 짝) | ||
| 테두리 두께borderWidth | 1px | ||
| 그림자shadow | shadow-s30 8px 24px -4px rgba(15, 18, 28, 0.10), 0 2px 6px -2px rgba(15, 18, 28, 0.05) · 다크 0 8px 24px -4px rgba(0, 0, 0, 0.50), 0 2px 6px -2px rgba(0, 0, 0, 0.25), inset 0 1px 0 0 rgba(255, 255, 255, 0.08)다크 shadow-s3-dark0 8px 24px -4px rgba(0, 0, 0, 0.50), 0 2px 6px -2px rgba(0, 0, 0, 0.25), inset 0 1px 0 0 rgba(255, 255, 255, 0.08)떠 있는 높이 — 지금 손으로 쓴 0 10 30 18% + 0 2 8 8% 의 가장 가까운 토큰(Elevation Local 2 · 떠 있는 동작) | ||
| z-indexzIndex | 50z-sticky — specs/z-index.md L1. 스낵바(L6)가 위에 뜬다 | ||
| 전환 대상transitionProperty | height, margin, padding펼침 ↔ 줄어듦 | ||
| 전환 시간transitionDuration | motion-duration-d4200ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
item | enabled | 간격gap | 2px |
| 쌓는 방향direction | 세로아이콘 위 · 라벨 아래, 가운데 | ||
| 커서cursor | pointer | ||
| 터치 영역touchTarget | 칸 전체펼침 약 61 × 54 · 줄어듦 약 53 × 48(360 폭) — AA ✓ · AAA ✓(줄어든 바는 칸이 바 높이를 다 채운다) | ||
| tabMemorytabMemory | 마지막 화면 · 스크롤다른 탭을 다녀오면 그 탭의 마지막 화면 · 스크롤로(가계부 칸은 마지막에 보던 통계로) — 웹 · 앱 같다. 웹은 목록이 그려진 뒤 돌려놓고 못 하면 맨 위(2026-10-16 사용자 결정 Q-018-4 A). 지금 탭을 다시 누르면 첫 화면 · 맨 위 | ||
icon | enabled | 아이콘size | 24pxlucide 선 아이콘(v106 단계 — 지금 22 를 24 로) |
| 색color | fg-neutral-subtle#62697A다크 #A2A8B7 | ||
| 선 굵기strokeWidth | 2pxlucide 기본 | ||
label | enabled | 글자·아이콘foreground | fg-neutral-subtle#62697A다크 #A2A8B7 |
| 글자typography | text-t1-static11 / 15 — 글자 크기 설정을 따르지 않는다(웹 text-t1-static, 앱 TextScaler.noScaling — SEED). 지금 10.5 를 11 로 | ||
| 굵기fontWeight | 500고르든 안 고르든 500 — 고를 때 폭이 흔들리지 않는다 | ||
| 줄 수lineClamp | 1한 줄 — 라벨은 한글 5자 이내로 쓴다 | ||
addButton | enabled | 크기size | 44px원 — 칸 가운데. 누르는 영역은 칸 전체 |
| 모서리radius | radius-full9999px(알약) | ||
| 배경background | bg-brand-solidDesk#0147AD다크 #1049A4HR#075B3C다크 #1B583E브랜드 채움 — 두 모드 같다. 탭 바에서 브랜드 색은 + 하나뿐이다 | ||
| 그림자shadow | none | ||
addIcon | enabled | 크기size | 24pxlucide plus(v106 단계 — 지금 22 를 24 로) |
| 색color | static-white#FFFFFF | ||
| 선 굵기strokeWidth | 2.5px채운 원 위의 흰 + — 지금 값 그대로 | ||
notification | enabled | 자리position | 아이콘 상자 기준Notification Badge — 점은 x 17 ~ 23 · y 1 ~ 7, 숫자는 왼쪽 아래 (16, 14). 2칸까지 · 99+(SEED). 지금 쓰는 칸은 없다 |
bodyInset | enabled | 아래 여백paddingBottom | 바의 아래 자리 + 66 + 24마지막 줄이 바 위 24 에서 끝난다 — 안전 영역이 없으면 14 + 66 + 24 = 104, 34 면 28 + 66 + 24 = 118(지금 값). 줄어도 그대로 둔다 |
item | pressed | 배율scale | 칸 2px 거리 축소색은 그대로 — 손을 떼기 전에 고른 것처럼 보이지 않게(v104 · Tabs). 모션 줄이기면 하지 않는다 |
| 축소 시간scaleDuration | motion-duration-pressed-scale150ms | ||
| 축소 곡선scaleEasing | motion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1) | ||
addButton | pressed | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
| 배율scale | 2px 거리 축소기준은 원 지름 — 펼침 44 → 0.955 · 줄어듦 36 → 0.944(v104) | ||
focusRing | focused | 링 두께width | 2px |
| 모서리radius | radius-r312px | ||
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | -2px칸 안쪽 — 붙은 이웃 칸 · 바 가장자리에 걸리지 않는다. + 는 원 바깥 2 |
size=regular
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 높이height | 66px |
| 상하 여백paddingY | spacing-x1_56px | ||
| 좌우 여백paddingX | spacing-x2_510px | ||
| 아래 바깥 여백marginBottom | max(14px, 아래 안전 영역 − 6px)홈 표시줄이 있으면 34 − 6 = 28 — 바의 아래 여백 6 만 안전 영역에 걸치고 누르는 칸은 안전 영역 위에서 끝난다 | ||
| 좌우 바깥 여백marginX | spacing-x3_514px화면 끝 → 바 14(좌우 안전 영역을 더한다) | ||
addButton | enabled | 크기size | 44px |
addIcon | enabled | 크기size | 24px |
size=compact
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 높이height | 48px |
| 상하 여백paddingY | 0px칸이 바 높이 48 을 다 채운다 — 누르는 영역 44 이상(v106). 보이는 모양은 그대로 | ||
| 좌우 여백paddingX | spacing-x28px | ||
| 아래 바깥 여백marginBottom | max(12px, 아래 안전 영역 − 8px)홈 표시줄이 있으면 26 — 칸(48)은 안전 영역 위에서 끝난다 | ||
| 좌우 바깥 여백marginX | spacing-x936px화면 끝 → 바 36 | ||
label | enabled | 보이기display | 보이지 않게(sr-only)보이는 라벨만 숨긴다 — 글은 남아 칸의 이름이 된다(앱 Semantics label 그대로) |
addButton | enabled | 크기size | 36px원 36 |
addIcon | enabled | 크기size | 20pxv106 단계 — 지금 18 을 20 으로 |
selected=selected
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 색color | fg-neutral#1A1F2E다크 #F5F6FA짙은 글자색 — 브랜드 색이 아니다(결정 5A · Tabs 와 같은 '고름 = 짙은 색') |
| 선 굵기strokeWidth | 2.5pxv106 — 켜짐 · 선택은 선 2.5 | ||
label | enabled | 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA |
SEED 와 다른 점
- 떠 있는 알약(사용자 결정 2026-10-04, 4B) — SEED 는 화면 아래에 붙은 바(48 + 안전 영역 · 위 1px 선 · 안쪽 최대 480)다. groo 는 지금 모양(66 · 좌우 14)을 지키고, 바탕을 불투명하게 · 줄어도 이름을 남기고 · 줄어든 바를 누르면 펴지게 고쳤다.
- 줄어든다 — SEED 에 없는 동작이다(아래로 20 · 위로 28 · 맨 위 40 — 지금 Desk 웹 · 앱 값).
- 선 아이콘 + 짙은 색 · 선 굵기 — SEED 는 채움(Fill) 아이콘 + 색 톤이다. lucide 에는 채움 모양이 없어 v106 대로 굵기 · 색으로 고름을 보인다. SEED 그림의 안 고른 색(아이콘 #B0B3BA 2.10:1 · 라벨 #868B94 3.42:1)은 대비가 모자라
fg-neutral-subtle(5.50)이다. - 가운데 + 는 브랜드 원 44, 라벨 없음 — SEED 의 가운데 글쓰기 버튼은 글로벌 앱의 탭 칸 하나(둥근 사각 24 + 라벨)다. 이름은 화면마다 단다.
- 그림자 · 안쪽 테두리 — SEED 는 위 1px 선(1 기기 픽셀)뿐이다. groo 는 떠 있는 바라
shadow-s3+ 안쪽 1pxstroke-neutral-subtle이다(지금 손으로 쓴 값의 가장 가까운 토큰 — 새 토큰 없음). - 탭은 링크 +
aria-current, 탭마다 스크롤 기억 · 다시 누르면 맨 위 — SEED 문서는 역할 · 이름 · 키보드 · 다시 누르기를 적지 않았다. - z-index 는 specs/z-index.md 의 L1(
z-sticky50).
Migration notes
2026-10-22 — 기기 뒤로 · Desk 배치 소제목 · 이동 부품 표 링크
앱 기기 뒤로(탭 첫 화면 → 홈, 홈 → 닫기, 탭 이력 없음 — 2026-09-12 사용자 결정, 앱 branch_back_to_home.dart 에 이미 구현)를 "탭을 옮길 때" 에 적었다(7B). 다섯 칸의 이름 · 가계부 묶음은 "Desk 에서는" 소제목 아래로 — 부품 규칙(다섯 칸 이하 · 늘 같은 칸)은 위에 남는다(9B). 머리말의 다른 이동 부품 안내는 DESIGN.md Layout 의 "이동 부품 고르기" 링크로(6B). "탭 바가 있는 화면에 바닥 고정 버튼을 두지 않는다" 는 그대로 두고 까닭 · 범위(탭 바가 없는 화면은 둘 수 있다)를 밝혔다(남은 규칙 R20 — 그대로). 사용자 결정 — https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L. 문장 이동 · 덧붙임만, 값 변경 없음.
2026-10-21 — 단위(03-G 1A)
웹 + 원 size-11 · 아이콘 size-6(rem)이 글자 200% 에서 88 로 커져 바(66) 밖으로 넘쳤다. 레시피를 size-[44px] · [36px] · [24px] · [20px] 로. 비교 페이지 https://claude.ai/artifact/A3oCdxNvDwkNBPFPE36bmF.
2026-10-16 — 웹도 탭마다 기억
구현 세션 질문 Q-018-4 를 사용자가 정했다(비교 페이지) — 웹 하단 탭도 앱처럼 그 탭의 마지막 화면 + 스크롤로 돌아온다(A). 목록이 그려진 뒤 돌려놓고, 못 하면 맨 위다. 늘 맨 위 · 첫 화면(B) · 마지막 화면만(C)은 고르지 않았다. 가계부 묶음 안의 1차 탭(가계부 · 자산 · 통계 · 예산)을 오갈 때의 기억은 Tabs 7A 대로다(정리).
- Desk 웹 — 칸의
href가 늘 그 탭의 첫 화면이다(가계부 칸은/desk/expense). 탭마다 마지막 경로 · 스크롤(경로별)을 기억하고, 돌아오면 데이터가 그려진 뒤 돌려놓는다. 하단 탭을 옮길 때는 초점만 그 화면의 제목으로 옮기고(focusScreenTitle({ resetScroll: false })— Top Navigation 레시피) 스크롤은 기억한 자리로 돌려놓는다 — 지금 임시 셸은 경로가 바뀔 때마다 맨 위로 보낸다.
2026-10-08 — 글자 값을 고정 px 토큰으로
YAML 의 라벨 글자가 rem 토큰($text-t1)을 가리키고 "글자 크기 설정을 따르지 않는 px" 는 비고에만 있었다 — YAML 대로 만들면 글자 크기 200% 에서 라벨이 칸 밖으로 넘친다. 레시피(text-t1-static) · 비고(앱 TextScaler.noScaling)와 같게 값 자리를 -static 토큰($text-t1-static)으로 고쳤다 크기 · 모습은 그대로다. -static 은 같은 글자 토큰의 px 판이다(Typography v104 — 내보낼 때 생긴다). Notification Badge · Wheel Picker 에 대한 사용자 결정(비교 페이지 3)을 같은 꼴의 이 자리에도 따랐다.
2026-10-04 — 떠 있는 알약을 Bottom Navigation 으로 정한다
사용자가 화면 틀 · 이동 비교 페이지에서 정했다 — 바는 떠 있는 알약을 지키고 고친다(4B — 바탕 불투명 bg-layer-floating · 흐림 걷음, 줄어들 때(48)도 칸마다 이름, 줄어든 바를 누르면 펴짐 — 앱도), 지금 탭은 짙은 색 + 선 2.5 · 다른 탭 fg-neutral-subtle + 선 2(5A), 가운데 + 는 브랜드 원 44 · 라벨 없음 · 이름은 화면마다(6B — 앱도 이름을 단다), 가계부 묶음은 탭 바 그대로 · 위 Line Tabs(7A). 그리고 "따라오는 것" — 5칸 이하 · 라벨 한글 5자 이내 · 라벨 11 / 15 고정 크기 · 최대 480 · 탭마다 스크롤 기억 · 지금 탭을 다시 누르면 맨 위 + 첫 화면 · 탭은 링크 + aria-current(앱 selected) · 바 이름 "주 메뉴". 붙은 바(4A) · 브랜드 색 고름(5B) · 라벨 붙은 + 칸(6A) · + 를 떠 있는 버튼으로(6C) · 탭 바 칸 바꿈(7B)은 고르지 않았다.
제품은 앱 적용 단계에서 옮긴다(2026-10-04 조사 — Desk 웹은 크로미움, Desk 앱은 위젯 테스트로 쟀다).
- 반투명 바 — 표면 82% + 흐림 20 · 채도 1.7(웹
shared/ui/groo/tabbar.tsx:111-128· 앱shared/widgets/p_tab_bar.dart:159-224). 짙은 · 브랜드 카드가 뒤로 지나가면 라벨 대비가 4.04 · 3.82, 줄어든 바 3.1 · 2.74, 다크 3.28 · 3.02 까지 떨어진다(F22). 불투명bg-layer-floating+shadow-s3로. - 줄어든 바의 칸 이름이 없다 — 웹은 라벨을 지워 네 칸이 이름 없는 버튼(52.8 × 40), 앱은 60 × 22 이고(웹
tabbar.tsx:186· 앱p_tab_bar.dart:309-329, F5), 앱은 줄어든 바에서 탭을 눌러도 펴지지 않는다(p_tab_bar.dart:216-223— 바깥 GestureDetector 가 InkWell 에 진다, F11). - 낭독 — 웹
nav에 이름이 없고 탭에aria-current가 없으며, + · ← 안의 SVG 가 이름 없는 그림으로 드러난다(tabbar.tsx:155-272, F17). 앱은 탭이 버튼 · 선택 상태 없이 라벨 + 누르기뿐이고, + 와 가계부 ← 에 이름이 0 이다(p_tab_bar.dart:283-409, F6). - 지금 탭이 브랜드 색 · 굵기 600 —
--fg-brand-strong(웹tabbar.tsx:172-176), 앱은 색만. 짙은 색 + 선 2.5 · 굵기 500 그대로로. 아이콘 22 → 24, 라벨 10.5 → 11. - 가계부 묶음의 칸 바꿈 — 가계부 · 자산 · 통계 · 예산에 들어가면 바가 ← · 가계부 · 자산 · 통계 · 예산으로 바뀌고(웹
widgets/layout/ui/AppTabBar.tsx:24-109· 앱p_tab_bar.dart:375-409), ← 는 홈으로 새로 이동한다(AppTabBar.tsx:51push · 앱goBranch(0)). 칸 바꿈 · 모드 전환 안무를 걷고 가계부 화면 위 Line Tabs 로. - 다시 누르기 · 스크롤 — 웹은 지금 탭을 다시 누르면 같은 주소가 쌓이고(히스토리 2 → 3 → 4) 맨 위로 가지 않으며, 탭을 옮겨도 앞 탭의 스크롤을 끌고 간다(
AppTabBar.tsx:53-58·AppLayout.tsx:93, F3 · F10). 앱은 탭마다 기억하지만 다시 눌러도 맨 위로 가지 않는다(mobile_scaffold.dart:146-168). 탭마다 기억 + 다시 누르면 맨 위 · 첫 화면으로. - + 이름 — 웹은
fabFor(widgets/layout/model/fab.ts:25-30)로 "거래 추가" · "일정 추가" 를 주고, 앱은 이름이 0 이다. 앱도 같은 이름을.
Badge
대상의 상태 · 분류를 한두 낱말로 보이는 작은 라벨("예정" · "연체" · "승인"). 누르지 않는다 — 고르고 거르는 것은 Chip, 실행은 Button, 시간 · 개수 · 길이 같은 메타 정보는 Tag Group, 안 읽은 알림은 Notification Badge 다.
Bottom Sheet
화면 아래에서 올라오는 모달. 1280 미만에서 폼 · 상세 · 고르기를 띄운다 — 같은 내용을 1280 이상에서는 Dialog 로 띄운다(한 부품이 폭으로 바꾼다 — Responsive Dialog). 되돌릴 수 없는 확인은 Alert Dialog, 들어간 화면 머리 ⋮ 의 동작 목록은 Menu Sheet, 화면 높이의 90% 를 넘는 내용은 페이지다.