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:
2026-10-13 14:44:21 +09:00
co-authored by Claude Opus 5.5
parent 68e82f9de9
commit 3fc50044e1
19 changed files with 227 additions and 63 deletions
+78 -27
View File
@@ -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 {
+1 -1
View File
@@ -174,7 +174,7 @@ const SCAFFOLD_CSS = `
font-size: var(--text-body-sm);
color: var(--color-text-muted);
}
.b-scaffold-wf { height: calc(100dvh - 64px - 56px); }
.b-scaffold-wf { height: calc(100dvh - var(--app-header-h) - 56px); }
.b-pending {
display: flex;
+9
View File
@@ -83,3 +83,12 @@ html:is([data-size="compact"], [data-size="medium"])
html:not([data-touch="on"]) .ui-touch-only {
display: none !important;
}
/* 위 줄 높이(6-1) — 화면 높이 계산이 모두 이 값을 씀 · ☰ 가 메뉴를 맡는 로그인 뒤 쪽(B · M)은 위 줄을 걷어 0 */
:root {
--app-header-h: 64px;
}
html.is-bm-route[data-burger="on"] {
--app-header-h: 0px;
}
+8 -1
View File
@@ -7,6 +7,7 @@
* - data-input = 마지막 누름의 pointerType(mouse · touch · pen)
* - data-touch-mode = 사용자 스위치(auto · on · off · 이 기기 브라우저에만 저장)
* - data-touch = on · off — 누를 자리 크기를 키울지(auto 면 주 입력 coarse 일 때)
* - data-burger = on · off — ☰ 서랍이 메뉴를 맡는지(폭 900 이하 · 터치) — 로그인 뒤 쪽은 위 줄을 걷음(6-1)
* 바뀔 때만 window 에 `app:context` 이벤트(detail = 지금 상태).
* 경계 표는 여기 한 벌 — CSS 는 위 표시(data-size)로 가르고, 숫자가 필요하면 --bp-* 변수.
* 첫 화면 깜빡임 — 화면은 모두 JS 가 그리므로 main.ts 가 맨 먼저 import 하면 그리기 전에 적힘.
@@ -15,6 +16,8 @@
import "./device_context.css";
export const SIZE_BREAKS = { medium: 600, expanded: 840, large: 1200 } as const;
/** 위 메뉴가 숨는 폭(app_shell `@media (max-width: 900px)` 과 짝) — 이하면 ☰ 서랍 */
export const BURGER_MAX = 900;
export type SizeClass = "compact" | "medium" | "expanded" | "large";
export type TouchMode = "auto" | "on" | "off";
@@ -24,6 +27,7 @@ export interface DeviceContext {
input: string;
touchMode: TouchMode;
touch: boolean;
burger: boolean;
}
const TOUCH_MODE_KEY = "frd_touch_mode";
@@ -54,12 +58,14 @@ let lastInput = "";
function read(): DeviceContext {
const pointer = coarseQuery.matches ? "coarse" : "fine";
const touchMode = readTouchMode();
const touch = touchMode === "on" || (touchMode === "auto" && pointer === "coarse");
return {
size: sizeClassOf(window.innerWidth),
pointer,
input: lastInput || (pointer === "coarse" ? "touch" : "mouse"),
touchMode,
touch: touchMode === "on" || (touchMode === "auto" && pointer === "coarse"),
touch,
burger: touch || window.innerWidth <= BURGER_MAX,
};
}
@@ -72,6 +78,7 @@ function apply(): void {
html.dataset.input = next.input;
html.dataset.touchMode = next.touchMode;
html.dataset.touch = next.touch ? "on" : "off";
html.dataset.burger = next.burger ? "on" : "off";
const first = !current;
current = next;
if (!first) window.dispatchEvent(new CustomEvent("app:context", { detail: next }));
+1 -1
View File
@@ -119,7 +119,7 @@
flex-direction: column;
width: 100%;
max-width: none;
min-height: calc(100dvh - var(--spacing-64));
min-height: calc(100dvh - var(--app-header-h));
}
.b04-surface__workspace > .b04-surface__status {
+2 -2
View File
@@ -1,6 +1,6 @@
.b05-route-layout {
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--app-header-h));
height: calc(100dvh - var(--app-header-h));
min-height: 0;
overflow: hidden;
}
+2 -2
View File
@@ -9,8 +9,8 @@
/* --- 좌측 입력 폼 --- */
.b06-profile-layout {
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--app-header-h));
height: calc(100dvh - var(--app-header-h));
min-height: 0;
overflow: hidden;
}
@@ -5,8 +5,8 @@
/* 워크플로우 레이아웃 높이 (B06 패턴 준수) */
.b07-design-layout {
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--app-header-h));
height: calc(100dvh - var(--app-header-h));
min-height: 0;
}
@@ -4,7 +4,7 @@
display: flex;
flex-direction: column;
gap: var(--spacing-8);
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--app-header-h));
padding: var(--spacing-16) var(--spacing-24);
box-sizing: border-box;
}
@@ -5,7 +5,7 @@
표 양식일 때는 페이지 쪽 모양 그대로 두려고 도면을 품은 때만 건다. */
.m02-main:has(> .m02-main__host > .m02-drawing) {
box-sizing: border-box;
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--app-header-h));
min-height: 0;
}
@@ -40,7 +40,7 @@
/* 카드가 보이면 상세 칸은 창 높이까지 — 표 + 카드가 넘치면 상세 칸 전체가 스크롤 */
.m02-main:has(> .m02-main__extra > .m02-struct:not([hidden])) {
box-sizing: border-box;
max-height: calc(100dvh - var(--spacing-64));
max-height: calc(100dvh - var(--app-header-h));
overflow-y: auto;
}
@@ -159,7 +159,7 @@
/* ? 도움말 상자 — 카드 오른쪽 위(제목 줄 · 부제 아래 · 도면 · 엑셀 카드) */
.m02-struct__card > .m02-struct__keys {
top: calc(var(--spacing-64) + var(--spacing-8));
top: calc(var(--app-header-h) + var(--spacing-8));
}
.m02-struct__keys {
position: absolute;
@@ -137,7 +137,7 @@
/* 면적산출 · 재료산출 전체 편집 — `.ss` 는 height:100% 로 부모 높이를 받음 · 도면 편집과 같이 메인 칸을 화면 높이로 채움 */
.m02-main:has(> .m02-main__host > .ss) {
box-sizing: border-box;
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--app-header-h));
min-height: 0;
}
@@ -155,7 +155,7 @@
/* 일위대가 상세(85-5) — 면적 상세와 같은 고정 높이 · 본문 칸만 굴림 · 오른쪽 패널은 칸 높이(따로 스크롤) */
.m02-main:has(> .m02-main__host > .ui-side-panels-row) {
box-sizing: border-box;
height: calc(100dvh - var(--spacing-64));
height: calc(100dvh - var(--app-header-h));
min-height: 0;
}
+1
View File
@@ -117,6 +117,7 @@ AUDIT_JS = r"""
}
out.pointer = matchMedia('(pointer: coarse)').matches ? 'coarse' : 'fine';
out.rendered = !!(outlet && outlet.childElementCount);
out.outlet_top = outlet ? Math.round(outlet.getBoundingClientRect().top) : null; // 본문 위 끝(위 줄 걷힘 = 0 · 6-3)
return out;
}
"""
+99
View File
@@ -0,0 +1,99 @@
"""☰ 서랍 · 위 줄 걷기(PLAN_기기대응 6 · `app_shell.ts` · `ui_template_workflow_layout.ts`) — 실제 브라우저.
① 휴대폰 · 로그인 뒤 B 쪽 = 위 줄 걷힘(본문 위 끝 0) · ☰ 는 위 오른쪽 모서리 · 떠 있는 진행 판 없음
② ☰ 서랍 = 로고 · 홈 ─ 진행 단계 ─ 메뉴 ─ 언어 · 테마 · 로그인 · 터치 모드 차례 · 진행 단계는 진행 판의 **그 요소**(한 벌)
지금 단계 강조 그대로 · 닫으면 제자리(진행 판)로 돌아감 · 단계 누름 = 이동 + 서랍 닫힘
③ 휴대폰 · 공개 쪽(A01) = 위 줄 그대로(본문 위 끝 64)
④ 데스크탑(1440 · 마우스) = 위 줄 · 진행 판 그대로 · ☰ 없음
"""
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent))
import test_device_audit as audit # noqa: E402
OUTLET_TOP = "Math.round(document.getElementById('app-outlet').getBoundingClientRect().top)"
GROUPS = "[...document.querySelectorAll('.app-drawer > *')].map(e => e.className)"
def _ctx(browser, cred, size):
spec = audit.SIZES[size]
ctx = browser.new_context(
viewport=spec["viewport"], has_touch=spec["touch"], is_mobile=spec["touch"]
)
audit._login(ctx, cred)
pid = audit._project_id(ctx)
ctx.add_init_script(f"localStorage.setItem('frd_current_project_id', {json.dumps(pid)})")
return ctx
@pytest.mark.skipif(not audit._server_up(), reason="sub1 화면 서버(5174) 꺼짐")
def test_drawer_holds_steps_and_header_folds():
from playwright.sync_api import sync_playwright
cred = json.loads((audit.ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8"))
with sync_playwright() as p:
browser = p.chromium.launch(channel="msedge", headless=True)
page = _ctx(browser, cred, "phone_390x844_touch").new_page()
audit._open(page, "b07-design-detail")
# ①
assert page.evaluate(OUTLET_TOP) == 0
burger = page.evaluate(
"(() => { const r = document.querySelector('.app-burger').getBoundingClientRect();"
" return [Math.round(r.right), Math.round(r.top), Math.round(r.width)]; })()"
)
assert burger[0] >= 390 - 12 and burger[1] <= 12 and burger[2] >= 44, burger
assert page.evaluate(
"!document.querySelector('.ui-workflow-overlay__panel--progress').getClientRects().length"
)
# ②
page.evaluate(
"window.__steps = document.querySelector('.ui-workflow-overlay__panel--progress .ui-workflow-layout__steps')"
)
page.evaluate("document.querySelector('.app-burger').click()")
groups = page.evaluate(GROUPS)
order = [
next((k for k in ("brand", "steps", "nav", "touch") if k in c), "settings")
for c in groups
]
assert order == ["brand", "steps", "nav", "settings"], groups
assert page.evaluate(
"document.querySelector('.app-drawer__steps').firstElementChild === window.__steps"
)
active = page.evaluate(
"document.querySelector('.app-drawer__steps .ui-workflow-layout__step.is-active')?.textContent"
)
assert active and "상세" in active, active
page.evaluate("document.querySelector('.app-drawer__scrim').click()") # 닫기 = 제자리
assert page.evaluate(
"window.__steps.closest('.ui-workflow-overlay__panel--progress') !== null"
)
page.evaluate("document.querySelector('.app-burger').click()")
page.evaluate(
"[...document.querySelectorAll('.app-drawer__steps .ui-workflow-layout__step.is-enabled')]"
".find(b => b.textContent.includes('수량')).click()"
)
page.wait_for_function("location.hash.includes('b08-quantity')", timeout=10000)
assert not page.evaluate(
"document.querySelector('.app-header')?.classList.contains('is-drawer-open')"
)
# ③
audit._open(page, "a01-home")
assert page.evaluate(OUTLET_TOP) == 64
# ④
desk = _ctx(browser, cred, audit.DESKTOP).new_page()
audit._open(desk, "b07-design-detail")
assert desk.evaluate(OUTLET_TOP) == 64
assert not desk.evaluate("document.querySelector('.app-burger').getClientRects().length")
assert desk.evaluate(
"document.querySelector('.ui-workflow-overlay__panel--progress').getClientRects().length > 0"
)
browser.close()
+1 -1
View File
@@ -155,6 +155,6 @@ def test_항목_추가_창_카드_격자_표_접기():
assert "grid-auto-rows: var(--m02-struct-card-h);" in css
assert "--m02-struct-card-h: min(640px, 70vh);" in css # 31-6 화면 높이에 맞춤
assert "flex: 1 0 var(--m02-struct-grid-min);" in css # 엑셀 격자가 카드 높이를 채움
assert "max-height: calc(100dvh - var(--spacing-64));\n overflow-y: auto;" in css
assert "max-height: calc(100dvh - var(--app-header-h));\n overflow-y: auto;" in css
main = (m02 / "M02_MasterTemplete_UI_Main.ts").read_text("utf-8")
assert "host.hidden = !host.hidden;" in main and 'title.classList.toggle("is-folded"' in main
+1 -1
View File
@@ -1,5 +1,5 @@
.ui-general-layout {
min-height: calc(100dvh - var(--spacing-64));
min-height: calc(100dvh - var(--app-header-h));
padding: var(--spacing-40) var(--spacing-24) var(--spacing-48);
background: var(--color-bg);
}
+10 -16
View File
@@ -4,11 +4,11 @@
.ui-workflow-overlay__panel {
position: fixed;
top: calc(var(--spacing-64) + var(--spacing-16));
top: calc(var(--app-header-h) + var(--spacing-16));
z-index: var(--z-dropdown);
display: flex;
flex-direction: column;
max-height: calc(100dvh - var(--spacing-64) - var(--spacing-32));
max-height: calc(100dvh - var(--app-header-h) - var(--spacing-32));
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-cards);
@@ -19,7 +19,7 @@
/* title 패널: 헤더 아래 전체 높이 도킹 사이드바 (열림: 본문 밀기 / 닫힘: 좌측으로 슬라이드 아웃) */
.ui-workflow-overlay__panel--title {
top: var(--spacing-64);
top: var(--app-header-h);
bottom: 0;
left: 0;
width: min(var(--wf-left-panel-width), calc(100vw - var(--spacing-48)));
@@ -303,9 +303,9 @@
@media (max-width: 860px) {
.ui-workflow-overlay__panel--progress {
top: calc(var(--spacing-64) + var(--spacing-8));
top: calc(var(--app-header-h) + var(--spacing-8));
right: var(--spacing-8);
max-height: calc(100dvh - var(--spacing-64) - var(--spacing-16));
max-height: calc(100dvh - var(--app-header-h) - var(--spacing-16));
}
}
@@ -525,7 +525,7 @@
}
/* 기기 대응(PLAN_기기대응 2-3) — 폭 840 미만(data-size compact · medium)만.
좌측 패널 = 서랍(본문 위에 겹침 · 덮개를 누르면 닫힘) · 진행단계 판 = 아래에서 올라오는 판(뒤 화면 계속 조작). */
좌측 패널 = 서랍(본문 위에 겹침 · 덮개를 누르면 닫힘). */
.ui-workflow-overlay__scrim {
display: none;
}
@@ -535,21 +535,15 @@ html:is([data-size="compact"], [data-size="medium"])
> .ui-workflow-overlay__scrim {
display: block;
position: fixed;
inset: var(--spacing-64) 0 0 0;
inset: var(--app-header-h) 0 0 0;
z-index: calc(var(--z-dropdown) - 1);
background: rgba(38, 17, 74, 0.35);
pointer-events: auto;
}
html:is([data-size="compact"], [data-size="medium"]) .ui-workflow-overlay__panel--progress {
top: auto !important;
right: 0 !important;
bottom: 0 !important;
left: 0 !important;
width: auto !important;
max-height: 50dvh;
flex-direction: column;
border-radius: var(--radius-cards) var(--radius-cards) 0 0;
/* ☰ 가 메뉴를 맡는 때(폭 900 이하 · 터치)는 떠 있는 진행 판 대신 ☰ 서랍 안 진행 단계 묶음(6-2) */
html[data-burger="on"] .ui-workflow-overlay__panel--progress {
display: none;
}
/* 접힌 서랍 — 화면 밖 내용은 안 보이게(손가락 · 읽기 프로그램이 밖의 단추를 잡지 않음) · 손잡이만 남김 */
+3 -3
View File
@@ -1,6 +1,6 @@
.ui-workflow-layout {
position: relative;
min-height: calc(100dvh - var(--spacing-64));
min-height: calc(100dvh - var(--app-header-h));
background: var(--color-bg);
}
@@ -108,7 +108,7 @@
}
.ui-workflow-layout__body {
min-height: calc(100dvh - var(--spacing-64));
min-height: calc(100dvh - var(--app-header-h));
transition: padding-left var(--transition-base);
}
@@ -132,7 +132,7 @@
.ui-workflow-layout__main {
width: 100%;
min-width: 0;
min-height: calc(100dvh - var(--spacing-64));
min-height: calc(100dvh - var(--app-header-h));
overflow: auto;
background: var(--color-canvas);
}
@@ -3,6 +3,7 @@ import { ROUTES } from "@config/config_frontend";
import { t } from "./ui_template_locale";
import { createWorkflowOverlays } from "./ui_template_overlay";
import { navigateTo } from "../A00_Common/router";
import { setDrawerSteps } from "../A00_Common/app_shell";
import { fetchDashboardMe } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
let dashboardRolePromise: Promise<string> | undefined;
@@ -198,6 +199,8 @@ export function createWorkflowLayout(options: WorkflowLayoutOptions): WorkflowLa
});
root.append(body, overlays.root);
// ☰ 서랍 진행 단계 묶음(6-2) — 같은 단계 목록을 서랍이 열릴 때 빌려 감(그리기 · 누름 한 벌)
setDrawerSteps(progressContent);
const steps = [
...progressContent.querySelectorAll<HTMLElement>(
".ui-workflow-layout__step:not(.ui-workflow-layout__step--home)",