feat(device): 로그인 뒤 ☰ 상태 위 줄 걷기 · ☰ 는 위 오른쪽 모서리에 떠 있음 · 진행 단계를 ☰ 서랍 묶음으로(같은 요소를 빌려 옴 · 한 벌) · 서랍 차례 로고 ─ 진행 단계 ─ 메뉴 ─ 설정 · 위 줄 높이 = --app-header-h 하나 · data-burger (기기대응 6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
+78
-27
@@ -66,19 +66,7 @@ function buildHeader(): HTMLElement {
|
||||
const header = document.createElement("header");
|
||||
header.className = "app-header";
|
||||
|
||||
// 로고 (클릭 시 홈)
|
||||
const brand = document.createElement("button");
|
||||
brand.className = "app-brand";
|
||||
brand.type = "button";
|
||||
const brandIcon = document.createElement("img");
|
||||
brandIcon.className = "app-brand__icon";
|
||||
brandIcon.src = BRAND_ICON_URL;
|
||||
brandIcon.alt = "";
|
||||
const brandName = document.createElement("span");
|
||||
brandName.className = "app-brand__name";
|
||||
brandName.textContent = L("App_BrandName");
|
||||
brand.append(brandIcon, brandName);
|
||||
brand.addEventListener("click", () => navigateTo(ROUTES.A01_HOME));
|
||||
const brand = buildBrand();
|
||||
|
||||
// 네비게이션 링크
|
||||
const nav = document.createElement("nav");
|
||||
@@ -111,6 +99,31 @@ function buildHeader(): HTMLElement {
|
||||
return header;
|
||||
}
|
||||
|
||||
/** 로고 (클릭 시 홈) — 위 줄 · ☰ 서랍 첫 묶음이 같이 씀 */
|
||||
function buildBrand(): HTMLButtonElement {
|
||||
const brand = document.createElement("button");
|
||||
brand.className = "app-brand";
|
||||
brand.type = "button";
|
||||
const brandIcon = document.createElement("img");
|
||||
brandIcon.className = "app-brand__icon";
|
||||
brandIcon.src = BRAND_ICON_URL;
|
||||
brandIcon.alt = "";
|
||||
const brandName = document.createElement("span");
|
||||
brandName.className = "app-brand__name";
|
||||
brandName.textContent = L("App_BrandName");
|
||||
brand.append(brandIcon, brandName);
|
||||
brand.addEventListener("click", () => navigateTo(ROUTES.A01_HOME));
|
||||
return brand;
|
||||
}
|
||||
|
||||
/* ☰ 서랍 진행 단계 묶음(6-2) — 프로젝트 화면이 진행 판에 그린 단계 목록 **그 요소**를 받아 둠.
|
||||
* 서랍을 열 때 서랍으로 옮기고 닫을 때 제자리(진행 판)로 — 그리기 · 누름 코드는 한 벌. */
|
||||
let drawerSteps: { node: HTMLElement; home: HTMLElement } | null = null;
|
||||
|
||||
export function setDrawerSteps(node: HTMLElement | null): void {
|
||||
drawerSteps = node?.parentElement ? { node, home: node.parentElement } : null;
|
||||
}
|
||||
|
||||
const NAV_ITEMS: [keyof typeof ui_locales, string][] = [
|
||||
["App_Nav_Program", ROUTES.A02_PROG_DETAIL],
|
||||
["App_Nav_Company", ROUTES.A03_COMP_DETAIL],
|
||||
@@ -146,20 +159,31 @@ function buildDrawer(header: HTMLElement): HTMLElement[] {
|
||||
drawer.setAttribute("aria-label", L("App_Menu"));
|
||||
const scrim = document.createElement("div");
|
||||
scrim.className = "app-drawer__scrim";
|
||||
const steps = document.createElement("div");
|
||||
steps.className = "app-drawer__group app-drawer__steps";
|
||||
const setOpen = (open: boolean): void => {
|
||||
header.classList.toggle("is-drawer-open", open);
|
||||
burger.setAttribute("aria-expanded", String(open));
|
||||
if (!drawerSteps) return;
|
||||
if (open) steps.append(drawerSteps.node);
|
||||
else if (steps.contains(drawerSteps.node)) drawerSteps.home.append(drawerSteps.node);
|
||||
};
|
||||
burger.addEventListener("click", () => setOpen(!header.classList.contains("is-drawer-open")));
|
||||
scrim.addEventListener("click", () => setOpen(false));
|
||||
drawer.addEventListener("keydown", (e) => e.key === "Escape" && setOpen(false));
|
||||
// 메뉴 · 로그인 항목을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠)
|
||||
// 로고 · 진행 단계 · 메뉴 · 로그인 항목을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠)
|
||||
drawer.addEventListener("click", (e) => {
|
||||
if ((e.target as Element).closest(".app-drawer__nav, .app-drawer__auth")) setOpen(false);
|
||||
const hit = (e.target as Element).closest("button");
|
||||
if (hit?.closest(".app-drawer__brand, .app-drawer__steps, .app-drawer__nav, .app-drawer__auth"))
|
||||
setOpen(false);
|
||||
});
|
||||
|
||||
// 묶음 차례 = 로고 · 홈 ─ 진행 단계(프로젝트 안) ─ 메뉴 ─ 언어 · 테마 · 로그인 · 터치 모드
|
||||
const brandRow = document.createElement("div");
|
||||
brandRow.className = "app-drawer__group app-drawer__brand";
|
||||
brandRow.append(buildBrand());
|
||||
const navList = document.createElement("nav");
|
||||
navList.className = "app-drawer__nav";
|
||||
navList.className = "app-drawer__group app-drawer__nav";
|
||||
navList.append(...buildNavButtons());
|
||||
const prefs = document.createElement("div");
|
||||
prefs.className = "app-drawer__row";
|
||||
@@ -178,7 +202,10 @@ function buildDrawer(header: HTMLElement): HTMLElement[] {
|
||||
const auth = document.createElement("div");
|
||||
auth.className = "app-drawer__row app-drawer__auth";
|
||||
void fillAuthActions(auth);
|
||||
drawer.append(navList, prefs, auth, buildTouchModeSwitch());
|
||||
const settings = document.createElement("div");
|
||||
settings.className = "app-drawer__group";
|
||||
settings.append(prefs, auth, buildTouchModeSwitch());
|
||||
drawer.append(brandRow, steps, navList, settings);
|
||||
return [burger, scrim, drawer];
|
||||
}
|
||||
|
||||
@@ -283,6 +310,7 @@ function buildFooter(): HTMLElement {
|
||||
* -------------------------------------------------------------------------- */
|
||||
export function renderShell(appRoot: HTMLElement, showFooter = true): HTMLElement {
|
||||
appRoot.innerHTML = "";
|
||||
drawerSteps = null; // 새 쪽이 진행 판을 세우면 다시 맡김
|
||||
const header = buildHeader();
|
||||
const outlet = document.createElement("main");
|
||||
outlet.className = "app-outlet";
|
||||
@@ -354,10 +382,10 @@ const SHELL_CSS = `
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
.app-outlet {
|
||||
min-height: calc(100dvh - 64px - 56px);
|
||||
min-height: calc(100dvh - var(--app-header-h) - 56px);
|
||||
}
|
||||
.app-outlet--without-footer {
|
||||
min-height: calc(100dvh - 64px);
|
||||
min-height: calc(100dvh - var(--app-header-h));
|
||||
}
|
||||
.app-footer {
|
||||
height: 56px;
|
||||
@@ -400,14 +428,12 @@ const SHELL_CSS = `
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.app-burger { display: inline-flex; }
|
||||
}
|
||||
html[data-touch="on"] .app-burger { display: inline-flex; }
|
||||
html[data-burger="on"] .app-burger { display: inline-flex; }
|
||||
.app-header.is-drawer-open { z-index: var(--z-overlay); }
|
||||
.app-header.is-drawer-open .app-drawer__scrim {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 64px 0 0 0;
|
||||
inset: var(--app-header-h) 0 0 0;
|
||||
background: rgba(38, 17, 74, 0.35);
|
||||
}
|
||||
.app-header.is-drawer-open .app-drawer {
|
||||
@@ -415,7 +441,7 @@ html[data-touch="on"] .app-burger { display: inline-flex; }
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
top: var(--app-header-h);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(320px, 100vw);
|
||||
@@ -436,12 +462,20 @@ html[data-touch="on"] .app-burger { display: inline-flex; }
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
.app-drawer__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
.app-drawer__group + .app-drawer__group {
|
||||
padding-top: var(--spacing-16);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.app-drawer__steps:empty { display: none; }
|
||||
.app-drawer__touch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding-top: var(--spacing-16);
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
@@ -453,6 +487,23 @@ html[data-touch="on"] .app-burger { display: inline-flex; }
|
||||
html[data-size="compact"] .app-header { padding: 0 var(--spacing-16); gap: var(--spacing-8); }
|
||||
html[data-size="compact"] .app-actions { display: none; }
|
||||
html[data-size="compact"] .app-burger { margin-left: auto; }
|
||||
/* 로그인 뒤 쪽(B · M) + ☰ — 위 줄을 걷고 ☰ 만 화면 위 오른쪽 모서리에 띄움(6-1) · 본문이 화면 높이를 다 씀 */
|
||||
html.is-bm-route[data-burger="on"] .app-header {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
z-index: calc(var(--z-dropdown) + 1);
|
||||
}
|
||||
html.is-bm-route[data-burger="on"] .app-header > :is(.app-brand, .app-nav, .app-actions) {
|
||||
display: none;
|
||||
}
|
||||
html.is-bm-route[data-burger="on"] .app-burger {
|
||||
position: fixed;
|
||||
top: var(--spacing-8);
|
||||
right: var(--spacing-8);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
html.is-bm-route[data-burger="on"] .app-drawer__brand .app-brand { align-self: flex-start; }
|
||||
`;
|
||||
|
||||
export function injectShellStyles(): void {
|
||||
|
||||
Reference in New Issue
Block a user