AppHeader
진행 중 (AppHeader component — work in progress)
- 모바일 앱 셸 상단 바. 상단 safe-area(노치)를 흡수하고, 좌측 텍스트 · 액션 버튼 · 알림벨 · 아바타를 갖는 앱 크롬.
- 기본형은 배경·하단선이 없습니다 — 셸 배경과 이어지고 하단 패딩도 0이라 높이 48px(예전 57px에서 축소). 배경(
solid)과 하단선(divider)은 각각 독립으로 켤 수 있어요(배경과 구분선). - 1depth / 2depth 두 형태가 있습니다.
onBack을 넘기면 2depth로, 좌측에 백버튼이 붙고 제목이 강조돼요(깊이). - ⚠️ 탭도 이 컴포넌트가 그립니다. 모바일에서 상단 내비게이션은 헤더 줄 + 탭 줄이 하나예요 — 웹처럼 본문에
nav.ds-tab-nav를 따로 두지 않습니다(탭 줄). - 슬롯은 전부 옵션으로 켜고 끕니다 —
onBack/showBell/showAvatar/getActions. 우측 버튼 추가는 배열에 항목을 더하는 것으로 끝나요. - 모든 색·크기·타이포는
.ds-app-header*토큰에서 옵니다 (인라인 스타일·하드코딩 hex·!important없음). - 현재 소비 앱은 employee-app 하나입니다.
규칙 (강제) · 코딩 에이전트용 — 사람은 접힌 채로 두어도 됩니다
AppHeader 강제 규칙. 상세는 아래 본문.
⚠️ 트랩 1 — PageHeader와 다른 컴포넌트입니다. 섞어 쓰지 마세요.ds-page-header는 데스크톱 크롬이에요 — 3영역(타이틀바/툴바), 팀 셀렉터, 프로필 드롭다운, 44px 헤더 높이 정렬이 전제입니다. 모바일에 그대로 쓰면 툴바가 넘칩니다. 반대로 .ds-app-header를 데스크톱에 쓰면 safe-area 패딩이 의미 없이 붙어요. 화면 폭이 아니라 "무슨 셸인가"로 고르세요.
마크업
- 호스트가
x-data="appHeader({...})"로 감싸고packages/components/html/app-header.html을 include 합니다. 손으로 짓지 마세요. - 래퍼에
class="contents"가 필요합니다. 앱 셸 컨테이너가display:flex; flex-direction:column이면x-data래퍼<div>가 flex 자식으로 끼어들어 헤더 높이 계산이 어긋납니다.display:contents로 자식<header>를 직접 flex 자식으로 올리세요. - 알림벨은 중첩 include 로 들어갑니다.
htmlInclude()는 재귀 처리하지만 경로 기준(root)은 앱 디렉터리예요 — 그래서 partial 안에서도"../../packages/components/html/notification-bell.html"처럼 앱 기준 상대경로를 씁니다. 컴포넌트 기준 상대경로(./notification-bell.html)로 쓰면 빌드가 파일을 못 찾습니다.
⚠️ 트랩 2 — 아바타는 스토어 경로를 쓰세요. getFileUrl()을 직접 부르지 마세요getAvatarImage를 생략하면 컴포넌트가 스토어의 currentUserProfilePictureUrl(presigned·cache-busted)을 씁니다. PageHeader와 같은 경로이고, 사진을 교체했을 때 refreshProfilePictureUrl() 갱신이 자동으로 반영돼요. getFileUrl()을 직접 호출해 x-effect로 넣으면 그 갱신·캐시 무효화를 우회합니다(이 앱이 실제로 그랬고, 컴포넌트화하면서 걷어냈습니다).
기본형은 배경·하단선 없음, 하단 패딩 0padding: max(env(safe-area-inset-top, 0px), 12px) 16px 0. 상단만 safe-area와 12px 중 큰 값을 갖습니다.
- 초기 구현은
12px 16px 8px+ 흰 배경 + 하단선 1px = 57px였고, 기본형은 지금 48px입니다. - 아래 콘텐츠와의 간격은 콘텐츠가 갖습니다. 헤더에 하단 패딩을 되돌리면 화면마다 간격이 이중으로 붙어요.
- 배경·하단선이 필요하면 CSS를 고치지 말고
solid·divider옵션을 쓰세요 (배경과 구분선). 앱 CSS에서.ds-app-header에background를 덧칠하면 두 변형이 갈라집니다.
탭 줄 — 웹 탭과 다른 두 가지
- 배경 틴트 없음.
.ds-app-header__tabs안에서는 hover·pressed 배경을 깔지 않습니다(웹 TabNav는--ds-hover-tint-16/30사용). 터치 기기에서:hover가 탭한 뒤에도 남아 선택하지 않은 탭에 회색 박스가 붙은 것처럼 보이기 때문입니다. 선택 피드백은 글자색 + 밑줄이 담당해요.@media (hover: hover)로 가르지 마세요 — 데스크톱 브라우저 검수 화면과 실기기가 갈립니다 (배경 틴트 없음). - 활성 밑줄 오프셋이 1px 다릅니다. 웹 값(
ml-15px)은 nav 하단 1px 선에 겹치라고 내려간 값이라, 선이 없고overflow-y: clip이 걸린 헤더에서는 밑줄 아래가 잘립니다. DS가.ds-app-header__tabs안에서-14px로 보정해 두었으니 앱에서 다시 만지지 마세요.
알림벨 글리프 크기
.ds-app-header__bell > .relative > button > svg가 24px로 키웁니다(모바일 터치 타깃). 공유 벨은 다른 곳에서 20px 그대로예요.- 이전에는
[x-data="notificationBell"] > .relative > button > svg로 앱 CSS에서 잡았는데, 공유 템플릿의 4단 중첩 구조에 의존해 구조가 바뀌면 조용히 깨졌습니다. 클래스 훅으로 대체했으니 속성 셀렉터로 되돌리지 마세요.
시각 미리보기
라이브 미리보기 — 프로덕션과 동일한
.ds-app-header*클래스, 실제 앱 셸과 같은 430px 폭입니다. 2depth 체크박스로 백버튼과 제목 강조를, 나머지 체크박스로 슬롯(액션 · 벨 · 아바타)을 켜고 꺼 보세요. 우측은 액션 → 벨 → 아바타 순서로 고정입니다.배경이 없어 헤더가 셸 배경(회색) 위에 그대로 얹히는 것도 여기서 확인됩니다 — 아래 흰 본문 영역과의 경계에 선이 없어요.
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
데모의 한계
알림벨은 실제로는 Alpine 공유 컴포넌트(notificationBell)를 중첩 include 하는 자리라, 여기서는 같은 DOM 구조만 정적으로 재현했습니다 — 드롭다운은 열리지 않습니다. 상단 safe-area 패딩도 데스크톱 브라우저에서는 max(0px, 12px) = 12px로만 보입니다(노치 기기에서 더 커집니다).
단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/AppHeader.ts |
| HTML partial | packages/components/html/app-header.html |
| 스타일 | packages/static/styles/design-system.css (.ds-app-header*) |
| Alpine 등록 | appHeader (plugin.ts) |
토큰 소유권: color·typography·geometry(아바타 32px·gap 8px·safe-area 패딩)는 모두 DS 소유(
.ds-app-header*). arbitrary[...]·하드코딩 hex·새!important없음.
PageHeader와의 차이
두 컴포넌트는 이름이 비슷하지만 다른 셸을 위한 것입니다.
.ds-app-header (모바일) | .ds-page-header (데스크톱) | |
|---|---|---|
| 셸 | Capacitor 모바일 앱 | 브라우저 데스크톱 |
| safe-area | 상단 env(safe-area-inset-top) 흡수 | 없음 |
| 구성 | leading 텍스트 · 액션 · 벨 · 아바타 | 타이틀바 + 툴바(팀 셀렉터·프로필 드롭다운·divider) |
| 높이 | 내용에 따름 (패딩 기반) | 44px 정렬 전제 |
| 하단 짝 | BottomNav | LnbSidebar |
모바일 앱에 PageHeader를 넣으면 툴바가 넘치고, 데스크톱에 AppHeader를 넣으면 safe-area 패딩이 무의미하게 붙습니다.
구조
┌─ header.ds-app-header ───────────────────────────────────────┐
│ padding: max(env(safe-area-inset-top), 12px) 16px 0 │
│ 배경 없음 · 하단선 없음 · 총 높이 48px │
│ ┌─ .ds-app-header__row (flex, space-between, gap 8) ───────┐ │
│ │ ┌ .__lead ─────────────┐ .ds-app-header__actions │ │
│ │ │ [.__back 32px] (2depth)│ ├ [액션 버튼 …] │ │
│ │ │ .__leading │ ├ [.__bell 알림벨 24px] │ │
│ │ └────────────────────────┘ └ [.__avatar 32px 원형] │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘좌측 __lead는 백버튼 + 제목 묶음, 우측 __actions는 액션 → 벨 → 아바타 순으로 고정입니다.
깊이 (1depth / 2depth)
onBack을 넘기는지로 갈립니다. 별도 depth 옵션은 없어요 — 뒤로 갈 곳이 있으면 2depth라는 뜻입니다.
| 1depth | 2depth | |
|---|---|---|
| 조건 | onBack 없음 | onBack 지정 (+ showBack 미지정/true) |
| 좌측 | __leading 텍스트만 | __back 버튼 + __leading |
__leading 성격 | 보조 텍스트 (날짜 등) | 화면 제목 |
| 스타일 | b4(14px) · neutral-400 | b3(16px) · semibold · neutral-900 |
| 루트 클래스 | — | .ds-app-header--depth2 |
3depth 이상도 2depth와 같은 형태입니다. 깊이를 세지 않고 "뒤로 갈 곳이 있는가"만 봅니다 — 백버튼 + 제목이면 충분하고, 계층이 깊어져도 헤더 모양은 달라지지 않아요.
아래는 실제 렌더입니다(프로덕션과 동일한 클래스).
1depth — 보조 텍스트(날짜) + 벨 + 아바타
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
2depth 이상 — 백버튼 + 화면 제목
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
2depth + 탭 줄 — 하위 화면에 탭이 있는 경우
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
2depth에서 벨·아바타는 보통 끕니다(showBell: false, showAvatar: false) — 하위 화면에서 프로필로 빠지는 동선은 잘 안 쓰니까요. 위 예시도 꺼둔 상태입니다. 다만 강제는 아니라 필요하면 켤 수 있습니다.
헤더 하나가 목록과 상세를 겸할 때
앱 셸은 헤더 인스턴스가 하나인데 화면은 목록(1depth)과 상세(2depth)를 오갑니다. 이때는 onBack을 항상 넘기고 showBack으로 깊이를 가릅니다 — onBack 유무는 마운트 시점에 굳어 바꿀 수 없기 때문입니다.
<div class="contents" x-data="appHeader({
getLeading: () => headerLeading, <!-- 목록: 날짜 / 상세: 화면 제목 -->
onBack: () => depthBack(),
showBack: () => isDepth, <!-- 여기서 깊이가 갈린다 -->
showBell: () => !isDepth,
showAvatar: () => !isDepth,
getActions: () => headerActions, <!-- 상세 전용 액션(검색·멤버 등) -->
})">제목 강조(--depth2)는 백버튼과 한 몸이라 showBack 결과를 따라갑니다. 목록에서 제목만 커지는 일은 생기지 않습니다.
상세 상태는 어디에 두나
상세를 여는 쪽(섹션/컴포넌트 스코프)과 헤더(앱 셸 스코프)는 서로를 못 봅니다. 상태는 앱의 라우터 스토어에, 뒤로가기 같은 동작은 window 이벤트로 넘기세요. 스토어에 뒤로가기 함수를 담으면 섹션 스코프가 스토어로 새어나가고, 섹션이 둘 이상이면 누구 것인지 추적이 안 됩니다.
탭이 필요한 화면은 getTabs를 넘기면 헤더가 탭 줄까지 그립니다 — 별도 컴포넌트를 놓지 않습니다(탭 줄).
배경과 구분선
배경(solid)과 하단선(divider)은 서로 독립적인 on/off 축입니다. 넷 중 아무 조합이나 가능하고, 기본형은 둘 다 없음이에요.
| 조합 | 옵션 | 높이 (탭 없음) | 높이 (탭 있음) | 쓰임 |
|---|---|---|---|---|
| 기본형 | — | 48px | 48px + 탭 줄 | 셸 배경과 이어져 화면의 일부처럼 읽힘 |
| 배경만 | solid | 56px | 48px + 탭 줄 | 콘텐츠 위에 얹힌 면으로 읽힘 |
| 선만 | divider | 57px | 48px + 탭 줄 + 1 | 배경 없이 경계만 필요할 때 |
| 배경+선 | 둘 다 | 57px | 48px + 탭 줄 + 1 | 가장 또렷한 분리 |
하단 여백은 조건부입니다
배경이나 선이 있으면 헤더가 하나의 면이 되므로, 콘텐츠와의 하단 여백 8px을 헤더가 직접 갖습니다. 기본형(투명)은 셸과 이어지므로 그 여백이 콘텐츠 몫이라 0이고요.
단 탭 줄이 있으면 붙지 않습니다 — 탭이 그 아래를 채우기 때문입니다. 8px을 더하면 탭과 콘텐츠 사이가 벌어져요. 조건은 이렇습니다:
(solid 또는 divider) AND 탭 줄 없음 → padding-bottom: 8px판정은 컴포넌트(appHeaderRootClass)가 하고 CSS는 결과 클래스(--pad-bottom)만 받습니다. :has()에 기대지 않으려는 의도예요.
참고: 초기 구현의 57px이 정확히
divider+ 하단 8px 조합입니다. 처음부터 헤더는 "선 있는 변형"이었고, 그게 기본값처럼 굳어 있었던 셈이에요.⚠️
divider는border-bottom이라 높이에 1px이 더 붙습니다. 헤더 높이를 계산에 쓰는 곳이 있으면 감안하세요.
기본형 — 배경·선 없음 (48px)
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
배경만 — solid: true
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
선만 — divider: true
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
배경 + 선 — 둘 다 (57px)
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
배경 + 탭 줄 — 탭이 있으면 하단 8px이 붙지 않습니다
체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다
배경이 필요할 때 앱 CSS에서 .ds-app-header에 background를 덧칠하지 마세요. 두 변형이 갈라져 나중에 어느 화면이 어떤 상태인지 추적이 안 됩니다 — 옵션으로만 켜세요.
화면마다 다르면 함수로 넘기세요
앱 셸(employee-app)처럼 헤더 인스턴스 하나가 여러 섹션을 도는 구조에서는 solid·divider를 boolean이 아니라 () => boolean으로 넘깁니다. x-data 표현식은 마운트 시 한 번만 평가되므로, boolean을 넣으면 첫 화면의 값이 그대로 굳어 섹션을 바꿔도 배경이 따라오지 않습니다.
<!-- 메신저 섹션에서만 배경을 깐다(본문 맨 위가 흰 서브탭 바라 회색 띠가 남지 않도록) -->
<div class="contents" x-data="appHeader({
solid: () => activeSection === 'messenger',
})">판단 기준은 본문 맨 위가 무슨 면인가입니다 — 흰 면(서브탭 바·카드)으로 시작하면 solid로 이어 붙이고, 회색 셸이 그대로 이어지면 기본형(투명)을 둡니다.
탭 줄
⚠️ 모바일에서 상단 내비게이션은 헤더와 탭이 한 컴포넌트입니다. 웹처럼 본문에 nav.ds-tab-nav를 따로 두지 마세요 — getTabs를 넘기면 헤더 줄 아래에 탭 줄이 붙습니다. 비우거나 생략하면 탭 줄 자체가 렌더되지 않습니다.
┌─ .ds-app-header ─────────────────┐
│ ‹ 연차 상세 🔔 (A) │ ← __row (헤더 줄)
│ 연차 근무 인센티브 서류 │ ← __tabs (탭 줄, getTabs 있을 때만)
└──────────────────────────────────┘appHeader({
getLeading: () => '결재',
getTabs: () => [
{ id: 'leave', label: '연차' },
{ id: 'work', label: '근무', badge: 2 },
],
getActiveTab: () => activeTab,
onTabSelect: (id) => switchTab(id),
})AppHeaderTab | 타입 | 설명 |
|---|---|---|
id | string | onTabSelect 인자이자 활성 비교 대상 |
label | string | 탭 라벨 |
badge | number | string | null | 후행 카운트. 0·null·"" 면 숨김 |
disabled | boolean? | 렌더하되 선택 차단 |
아이템 스타일은 웹 TabNav와 공유합니다 — .ds-tab-item + .ds-tab-item--ml(48px)을 그대로 씁니다. 형태를 웹과 맞추기 위해서예요.
- ⚠️ 활성 클래스는
tab-active입니다.is-active가 아니에요(BottomNav와 다릅니다 — 그쪽은is-active). - 사이즈는 현재
ml고정입니다. 모바일 전용 사이즈 베리에이션이 정해지면 여기에 축을 추가합니다. - 가로 스크롤은
.ds-app-header__tabs가 소유하며, 활성 밑줄이 박스를 넘어 세로 스크롤을 만드는 문제는overflow-y: clip으로 DS 안에서 막았습니다. 앱 CSS에 클립 보정을 따로 넣지 마세요(웹은 반대로.tabnav-unclip으로 클립을 푸는 opt-in 보정을 쓰는데, 그 규칙을 여기 복사하면 세로 스크롤이 되살아납니다). - 활성 밑줄 위치도 웹과 다릅니다. 웹 탭의 기본 오프셋(
ml-15px)은nav.ds-tab-nav의 하단 1px 선 위에 겹치라고 1px 더 내려간 값이에요. 헤더 탭 줄엔 그 선이 없고overflow-y: clip이 걸려 있어 그대로 두면 2px 밑줄의 아래가 잘립니다(실측2px → ~1.2px)..ds-app-header__tabs안에서 1px 올려(-14px) DS가 보정해 두었으니 앱에서 다시 만지지 마세요.
배경 틴트 없음
모바일 탭에는 hover·pressed 배경 틴트를 깔지 않습니다. 웹 TabNav는 --ds-hover-tint-16/30으로 회색 틴트를 쓰지만, .ds-app-header__tabs 안에서는 배경이 항상 투명입니다.
터치 기기에서 :hover는 탭한 뒤에도 다음 탭을 누를 때까지 남습니다(iOS·Android 공통). 그래서 틴트를 그대로 두면 선택하지도 않은 탭에 회색 박스가 붙어 있는 것처럼 보여요. 모바일에서 선택 피드백은 글자색 + 하단 밑줄이 이미 담당하므로 배경은 아예 뺐습니다.
@media (hover: hover)로 가르지 않았습니다
그 방식이면 데스크톱 브라우저로 앱을 열었을 때(개발·검수 환경) 여전히 틴트가 보여 화면이 두 갈래가 됩니다. 앱 셸 탭은 어디서 열든 "배경 없음" 하나예요. 웹 탭은 그대로 틴트를 씁니다 — 이 규칙은 .ds-app-header__tabs 안쪽에만 적용됩니다.
사용법
<div class="contents" x-data="appHeader({
getLeading: () => todayStr(),
showBell: true,
showAvatar: true,
getAvatarInitial: () => displayName.charAt(0),
onAvatarClick: () => switchSection('more'),
})">
<!-- include "../../packages/components/html/app-header.html" -->
</div>우측에 버튼을 더하려면 getActions만 넘기면 됩니다 — partial은 손대지 않습니다.
getActions: () => [
{ id: 'search', icon: 'ic-search', label: '검색', onClick: openSearch },
{ id: 'filter', icon: 'ic-filter', label: '필터', onClick: openFilter,
hidden: () => !canFilter },
]옵션
appHeader(options)
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
getLeading | () => string | "" | 좌측 텍스트. 1depth면 보조 텍스트, 2depth면 화면 제목 |
onBack | () => void | — | 지정하면 2depth — 좌측 백버튼 + 제목 강조 (깊이) |
showBack | boolean | (() => boolean) | true | onBack이 있어도 false면 그 화면만 1depth. 헤더 하나가 목록·상세를 겸할 때 (깊이) |
backLabel | string | "뒤로" | 백버튼 접근성 이름 |
solid | boolean | (() => boolean) | false | 흰 배경 채움 (배경과 구분선). 화면마다 다르면 함수로 |
divider | boolean | (() => boolean) | false | 하단 1px 선. 켜면 높이 +1px. 함수 가능 |
showBell | boolean | (() => boolean) | false | 공유 알림벨 렌더. 화면마다 다르면 함수로 |
onBellClick | () => void | — | 넘기면 드롭다운을 열지 않고 이 핸들러만 호출 — 알림을 2depth 화면으로 여는 모바일용 (알림 벨) |
showAvatar | boolean | (() => boolean) | false | 아바타 렌더. 화면마다 다르면 함수로 |
getAvatarImage | () => string | null | — | 이미지 URL 오버라이드. 생략 권장 (아래 참고) |
getAvatarInitial | () => string | 스토어 사용자명 첫 글자 | 이미지 없을 때 폴백 이니셜 |
onAvatarClick | () => void | — | 아바타 탭 |
getActions | () => AppHeaderAction[] | [] | 벨 앞에 놓일 아이콘 버튼들 |
getTabs | () => AppHeaderTab[] | [] | 탭 줄 — 항목이 있으면 헤더 아래에 렌더 (탭 줄) |
getActiveTab | () => string | "" | 활성 탭 id |
onTabSelect | (id: string) => void | — | 탭 선택 |
AppHeaderAction
| 필드 | 타입 | 설명 |
|---|---|---|
id | string | 고유 id. aria-label 폴백이기도 함 |
icon | string? | 레지스트리 아이콘 이름 |
iconSvg | string? | raw SVG — icon보다 우선 |
label | string? | 접근성 이름. 없으면 id |
onClick | () => void | 클릭 핸들러 |
hidden | () => boolean | 버튼을 목록에서 제거 |
아바타
getAvatarImage를 생략하면 스토어의 currentUserProfilePictureUrl을 씁니다. 이게 기본이자 권장 경로예요.
- presigned URL이고 cache-busted 입니다
- 사진 교체 시
refreshProfilePictureUrl()갱신이 자동 반영됩니다 - 이미지 404면
@error로 이니셜 폴백합니다 (PageHeader와 동일 처리)
이니셜은 getAvatarInitial을 주면 그걸 쓰고, 없으면 스토어 사용자명 첫 글자를 씁니다. 앱이 자체 표시명 규칙(displayName)을 가지면 넘겨 주세요.
알림 벨
showBell을 켜면 공유 NotificationBell이 붙고, 누르면 드롭다운이 열립니다. 데스크톱 기준 동작이에요.
모바일에서는 이 팝오버가 어차피 화면을 거의 다 덮습니다. 그래서 알림을 2depth 화면으로 여는 앱은 onBellClick을 넘기세요 — 드롭다운을 렌더하지 않고 그 핸들러만 부릅니다.
<div class="contents" x-data="appHeader({
onBack: () => depthBack(),
showBack: () => isDepth,
showBell: () => !isDepth,
onBellClick: () => openNotifications(), <!-- 드롭다운 대신 2depth 진입 -->
})">- 미읽음 배지는 두 모드가 공유합니다. 어느 쪽이든 같은
notificationBell스코프 안에서 렌더되므로 카운트 폴링이 그대로 살아 있어요. - 여는 화면 쪽은 앱 몫입니다 — 헤더의 백버튼·제목·하단 내비 숨김은 깊이 메커니즘이 이미 처리하므로, 앱은
depth만 세우면 됩니다. - ⚠️ 그 depth의 주인이 셸이라면 뒤로가기를 window 이벤트로 돌리지 마세요. 섹션이 듣고 있는
app-depth-back을 쏘면 엉뚱한 섹션이 자기 뒤로가기를 실행합니다. 셸이 연 화면은 셸이 직접 닫습니다.
앱 소유로 남는 것
| 항목 | 위치 | 이유 |
|---|---|---|
| leading 텍스트 내용 | 앱 (todayStr() 등) | 앱마다 다름 |
| 아바타 탭 동작 | onAvatarClick | 라우팅은 앱 소유 |
| 액션 버튼 목록 | getActions | 화면마다 다름 |
관련
- BottomNav — 같은 앱 셸의 하단 크롬
- PageHeader — 데스크톱 크롬 (혼동 주의, 차이)
- NotificationBell —
showBell로 켜지는 공유 벨