feat(device): 로그인 뒤 쪽은 데스크탑도 위 줄 대신 ☰ · ☰ 왼쪽 위 36px(손가락 높이 44) · 서랍 왼쪽에서 · 진행 판 대신 서랍 진행 단계 · 첫 줄 들여쓰기 = --app-burger-inset 하나 · 공용(좌측 패널 제목 · 일반 쪽 제목 · --wf-burger-pad) + 쪽마다 한 줄 · CAD 제목 줄은 틀 자리로 잼 · Esc 는 ☰ 포커스에서도 닫힘 (기기대응 7)

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 17:17:12 +09:00
co-authored by Claude Opus 5.5
parent 3fc50044e1
commit fbe0f982d3
18 changed files with 271 additions and 42 deletions
+29 -11
View File
@@ -143,8 +143,8 @@ function buildNavButtons(): HTMLElement[] {
}
/* -----------------------------------------------------------------------------
* ☰ 서랍(PLAN_기기대응 2-2) — 폭 900 이하(위 메뉴가 숨는 폭) · 터치 모드에서 보임.
* 위 메뉴 · 언어 · 테마 · 로그인 단추와 같은 항목 + 터치 모드 스위치. 데스크탑 마우스엔 안 보임.
* ☰ 서랍(PLAN_기기대응 2-2 · 7-1) — 공개 쪽은 폭 900 이하 · 터치 모드에서 · 로그인 뒤 쪽(B · M)은 늘 보임.
* 위 메뉴 · 언어 · 테마 · 로그인 단추와 같은 항목 + 터치 모드 스위치.
* -------------------------------------------------------------------------- */
function buildDrawer(header: HTMLElement): HTMLElement[] {
const burger = document.createElement("button");
@@ -170,7 +170,7 @@ function buildDrawer(header: HTMLElement): HTMLElement[] {
};
burger.addEventListener("click", () => setOpen(!header.classList.contains("is-drawer-open")));
scrim.addEventListener("click", () => setOpen(false));
drawer.addEventListener("keydown", (e) => e.key === "Escape" && setOpen(false));
header.addEventListener("keydown", (e) => e.key === "Escape" && setOpen(false)); // ☰ 에 포커스가 있어도 닫힘
// 로고 · 진행 단계 · 메뉴 · 로그인 항목을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠)
drawer.addEventListener("click", (e) => {
const hit = (e.target as Element).closest("button");
@@ -410,7 +410,7 @@ const SHELL_CSS = `
.app-nav { display: none; }
.app-actions { margin-left: auto; }
}
/* ☰ 서랍 — 폭 900 이하 · 터치 모드에서만 보임(데스크탑 마우스엔 없음) */
/* ☰ 서랍 — 공개 쪽은 폭 900 이하 · 터치 모드에서만 · 로그인 뒤 쪽(B · M)은 늘 */
.app-burger,
.app-drawer,
.app-drawer__scrim {
@@ -428,7 +428,7 @@ const SHELL_CSS = `
font-size: 20px;
cursor: pointer;
}
html[data-burger="on"] .app-burger { display: inline-flex; }
html:is([data-burger="on"], .is-bm-route) .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;
@@ -487,23 +487,41 @@ html[data-burger="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 {
/* 로그인 뒤 쪽(B · M) — 폭 · 입력과 상관없이 위 줄을 걷고 ☰ 만 화면 왼쪽 위 모서리에 띄움(7-1)
본문이 화면 높이를 다 씀 · 서랍은 왼쪽에서 열림 · 위 줄 항목은 모두 서랍에 있음 */
html.is-bm-route .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) {
html.is-bm-route .app-header > :is(.app-brand, .app-nav, .app-actions) {
display: none;
}
html.is-bm-route[data-burger="on"] .app-burger {
/* ☰ 폭 36(7-4) · 높이는 손가락 44 · 마우스 36 */
html.is-bm-route .app-burger {
min-width: var(--app-burger-w);
min-height: var(--app-burger-w);
position: fixed;
top: var(--spacing-8);
right: var(--spacing-8);
left: var(--spacing-8);
z-index: 1;
margin: 0;
padding: 0;
box-shadow: var(--shadow-lg);
}
html.is-bm-route[data-burger="on"] .app-drawer__brand .app-brand { align-self: flex-start; }
html.is-bm-route[data-touch="on"] .app-burger { min-height: 44px; }
html.is-bm-route .app-header.is-drawer-open .app-drawer {
left: 0;
right: auto;
border-left: 0;
border-right: 1px solid var(--color-border);
}
html.is-bm-route .app-drawer__brand {
min-height: 44px;
padding-left: calc(var(--app-burger-inset) - var(--spacing-16));
}
html.is-bm-route .app-drawer__brand .app-brand { align-self: flex-start; }
`;
export function injectShellStyles(): void {
+6 -2
View File
@@ -84,11 +84,15 @@ html:not([data-touch="on"]) .ui-touch-only {
display: none !important;
}
/* 위 줄 높이(6-1) — 화면 높이 계산이 모두 이 값을 씀 · ☰ 가 메뉴를 맡는 로그인 뒤 쪽(B · M)은 위 줄을 걷어 0 */
/* 위 줄 높이(6-1) — 화면 높이 계산이 모두 이 값을 씀 · 로그인 뒤 쪽(B · M)은 위 줄을 걷어 0(7-1)
☰ 폭(7-4) · 들여쓰기(7-2) = 왼쪽 위에 뜬 ☰(자리 8 + 폭) + 틈 8 · 그 자리에 걸치는 첫 줄이 이만큼 왼쪽을 비움 */
:root {
--app-header-h: 64px;
--app-burger-w: 36px;
--app-burger-inset: 0px;
}
html.is-bm-route[data-burger="on"] {
html.is-bm-route {
--app-header-h: 0px;
--app-burger-inset: calc(var(--spacing-8) * 2 + var(--app-burger-w));
}
+1 -1
View File
@@ -7,7 +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)
* - data-burger = on · off — 공개 쪽에서 ☰ 서랍이 메뉴를 맡는지(폭 900 이하 · 터치) · 로그인 뒤 쪽(B · M)은 늘 ☰(7-1)
* 바뀔 때만 window 에 `app:context` 이벤트(detail = 지금 상태).
* 경계 표는 여기 한 벌 — CSS 는 위 표시(data-size)로 가르고, 숫자가 필요하면 --bp-* 변수.
* 첫 화면 깜빡임 — 화면은 모두 JS 가 그리므로 main.ts 가 맨 먼저 import 하면 그리기 전에 적힘.
+5
View File
@@ -192,6 +192,11 @@
gap: var(--spacing-8);
}
/* ☰ 들여쓰기(7-2) */
html.is-bm-route .b05-route__view-controls {
left: max(var(--spacing-16), var(--wf-burger-pad, 0px));
}
/* 방위 나침반을 놓을 자리 — ISO 버튼 바로 아래(2026-09-03 사용자 지시).
버튼 줄 높이 34px + 위 여백 16px 아래에 붙인다. 위젯 모양·색은 공용
(`ui_template/ui_template_compass.css`)이고 여기서는 위치만 준다. */
@@ -138,6 +138,11 @@
background: var(--color-bg);
}
/* ☰ 들여쓰기(7-2) */
html.is-bm-route .b06-section__notice-bar {
padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24)));
}
.b06-section__notice-blocks {
flex: 1 1 auto;
min-width: 0;
@@ -450,6 +450,11 @@
min-height: 0;
}
/* ☰ 들여쓰기(7-2) — 탭 줄이 본문 맨 위 · CAD 제목 줄은 CAD 앱이 스스로(`host-burger.ts`) */
html.is-bm-route .b07-cad-stack > .cad-tabs {
padding-left: max(6px, var(--wf-burger-pad, 0px));
}
.b07-cad-stack > .cad-host {
flex: 1 1 0;
height: auto;
@@ -0,0 +1,4 @@
/* 앱 ☰ 자리 비키기(기기대응 7-2) — 값은 host-burger.ts 가 적음 · 틀 없이 띄우면 0 */
html .cad-titlebar {
padding-left: max(12px, var(--host-burger-pad, 0px));
}
@@ -0,0 +1,19 @@
/**
* 앱 ☰ 자리 비키기(기기대응 7-2) — 앱(B07)이 왼쪽 위에 띄운 ☰ 가 이 틀(iframe) 맨 위 줄에 걸치면
* 걸친 폭 + 틈 8 을 `--host-burger-pad` 로 적음(App.css `.cad-titlebar` 가 그만큼 왼쪽을 비움).
* 좌측 패널을 여닫으면 틀 폭이 바뀌어 resize 로 다시 잼 · 홀로 띄운 CAD(틀 없음)는 0.
*/
import './host-burger.css';
export function registerHostBurgerPad(): void {
const frame = window.frameElement;
if (!frame) return;
const update = (): void => {
const b = frame.ownerDocument.querySelector('.app-burger')?.getBoundingClientRect();
const f = frame.getBoundingClientRect();
const pad = b?.width && b.bottom > f.top ? Math.max(0, Math.ceil(b.right + 8 - f.left)) : 0;
document.documentElement.style.setProperty('--host-burger-pad', `${pad}px`);
};
update();
window.addEventListener('resize', update);
}
+2
View File
@@ -20,6 +20,7 @@ import { InputController } from './inputController/input-controller.ts';
import { registerAisloDrawingBridge } from './integration/aislo-drawing-bridge.ts';
import { registerAisloTableExport } from './integration/aislo-table-export.ts';
import { registerDxfClipPaste } from './integration/dxf-clip-paste.ts';
import { registerHostBurgerPad } from './integration/host-burger.ts';
import {
getActiveToolActor,
getCanvas,
@@ -158,6 +159,7 @@ function initApplication() {
registerAisloDrawingBridge();
registerAisloTableExport(); // 고른 범위 → 면적산출 표(M02 83장)
registerDxfClipPaste();
registerHostBurgerPad(); // 앱 ☰ 자리 비키기(기기대응 7-2)
registerCadDebugHook();
registerAutoSave();
const screenCanvasDrawController = new ScreenCanvasDrawController(context);
+5
View File
@@ -19,6 +19,11 @@
border-bottom: 1px solid var(--color-border);
}
/* ☰ 들여쓰기(7-2) */
html.is-bm-route .b08-quantity > .b08-quantity__tabs:first-child {
padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24)));
}
/* 구조물 집계표 보조 탭 줄 자리(170-4) — 단추는 높이만 채움 */
.b08-quantity__tabs--blank > .ui-btn {
visibility: hidden;
@@ -25,6 +25,11 @@
border-bottom: 1px solid var(--color-border);
}
/* ☰ 들여쓰기(7-2) */
html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24)));
}
/* 아래 탭 없는 위 탭 — 보이지 않는 단추 한 줄로 높이만 채움(탭 바꿀 때 몸통이 안 튐) */
.b09-estimation__tabs--blank > .ui-btn {
visibility: hidden;
@@ -47,6 +47,11 @@
gap: var(--spacing-8);
}
/* ☰ 들여쓰기(7-2) */
html.is-bm-route .m01-master__headbox {
padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24)));
}
.m01-master__summary {
color: var(--color-accent);
font-size: var(--text-body-sm);
@@ -78,6 +78,11 @@
gap: var(--spacing-12);
}
/* ☰ 들여쓰기(7-2) */
html.is-bm-route .m02-main__head {
padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24)));
}
.m02-main__title {
flex: 0 1 auto;
min-width: 3rem;
+88 -9
View File
@@ -122,6 +122,40 @@ AUDIT_JS = r"""
}
"""
# ☰ 상자와 겹치는 보이는 글 · 단추(7-3) — 왼쪽 위에 뜬 ☰ 아래 깔린 것만(맨 위에 보이는 것 · 같은 출처 iframe 안 포함)
BURGER_JS = r"""
() => {
const b = document.querySelector('.app-burger');
if (!b || !b.getClientRects().length) return [];
const B = b.getBoundingClientRect(), hits = [];
const scan = (doc, ox, oy) => {
const L = B.left - ox, T = B.top - oy, R = B.right - ox, D = B.bottom - oy;
const cands = new Set(doc.querySelectorAll('button, a[href], input:not([type=hidden]), select, textarea, [role=button], [role=tab]'));
const w = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
for (let n = w.nextNode(); n; n = w.nextNode()) if (n.nodeValue.trim()) cands.add(n);
for (const c of cands) {
const el = c.nodeType === 3 ? c.parentElement : c;
if (!el || el.closest('.app-header, .ui-toast')) continue;
if (el.checkVisibility && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) continue;
let r = el.getBoundingClientRect();
if (c.nodeType === 3) { const g = doc.createRange(); g.selectNodeContents(c); r = g.getBoundingClientRect(); }
const l = Math.max(r.left, L), t = Math.max(r.top, T), rr = Math.min(r.right, R), bb = Math.min(r.bottom, D);
if (r.width < 1 || r.height < 1 || rr - l < 1 || bb - t < 1) continue;
const top = doc.elementsFromPoint((l + rr) / 2, (t + bb) / 2).find((e) => !e.closest('.app-header'));
if (top && top !== el && !el.contains(top) && !top.contains(el)) continue;
hits.push(`${el.tagName.toLowerCase()}:${(c.nodeType === 3 ? c.nodeValue : el.textContent).trim().slice(0, 16)}`);
}
for (const f of doc.querySelectorAll('iframe')) {
const fr = f.getBoundingClientRect();
if (fr.right < L || fr.left > R || fr.bottom < T || fr.top > D) continue;
try { if (f.contentDocument?.body) scan(f.contentDocument, ox + fr.left + f.clientLeft, oy + fr.top + f.clientTop); } catch (e) {}
}
};
scan(document, 0, 0);
return hits;
}
"""
# 데스크탑 배치 서명 — 화면 안에 보이는 요소 사각형(정수) · 순서대로.
# 잡음 빼기 = 움직이는 중(CSS 애니메이션 · 로딩 원)인 요소와 그 안 · 화면 밖(스크롤 아래 1px 흔들림)
LAYOUT_JS = r"""
@@ -203,6 +237,7 @@ def run_audit(tag: str, pages=PAGES, sizes=SIZES) -> dict:
_open(page, route)
row = page.evaluate(AUDIT_JS)
row["errors"] = list(errors)
row["burger_hits"] = page.evaluate(BURGER_JS)
if size == DESKTOP:
page.screenshot(path=str(shots / f"{route}.png"))
(shots / f"{route}.layout.json").write_text(
@@ -211,7 +246,8 @@ def run_audit(tag: str, pages=PAGES, sizes=SIZES) -> dict:
result["pages"].setdefault(route, {})[size] = row
print(
f"{route:22} {size:24} hs={row['hscroll_px']:4} off={row['offscreen']:3} "
f"cov={row['covered']:3} small={row['small']:3} menu={row['menu_unreachable']}",
f"cov={row['covered']:3} small={row['small']:3} menu={row['menu_unreachable']} "
f"top={row['outlet_top']} burger={len(row['burger_hits'])}",
flush=True,
)
ctx.close()
@@ -237,6 +273,46 @@ def layout_diff(route: str, a="before", b="after") -> int:
)
def _rect(row):
tag, xywh = row.split("|")
return tag, tuple(int(v) for v in xywh.split(","))
def bm_moved(route: str, a="before", b="after") -> list:
"""로그인 뒤 쪽(B · M) 데스크탑 — 위 줄(64)을 걷어 세로는 올라가도 가로 자리(태그 · left · width)는 같아야 함(7-3).
원래 상자 가운데 지금 같은 가로 자리가 없는 것. 빼는 것 = 위 줄 · ☰ 띠(위 64 + 60 안 · 들여쓴 첫 줄) ·
없어진 떠 있는 진행 판(ASIDE) 안 · 화면 전체 상자."""
ra = _boxes(json.loads((ROOT / "tmp" / f"desktop_{a}" / f"{route}.layout.json").read_text()))
rb = _boxes(json.loads((ROOT / "tmp" / f"desktop_{b}" / f"{route}.layout.json").read_text()))
import collections
left = collections.Counter((t, r[0], r[2]) for t, r in map(_rect, rb))
missing = []
for row in ra:
t, r = _rect(row)
if left[(t, r[0], r[2])] > 0:
left[(t, r[0], r[2])] -= 1
else:
missing.append((t, r))
gone = [r for t, r in missing if t == "ASIDE"]
def inside(r, g):
return (
g[0] <= r[0]
and g[1] <= r[1]
and r[0] + r[2] <= g[0] + g[2]
and r[1] + r[3] <= g[1] + g[3]
)
return [
f"{t}|{r}"
for t, r in missing
if r[1] >= 64 + 60
and r[2] < SIZES[DESKTOP]["viewport"]["width"]
and not any(inside(r, g) for g in gone)
]
def _server_up() -> bool:
import urllib.request
@@ -254,17 +330,20 @@ def test_device_audit_all_pages_render_and_desktop_unchanged():
for size, row in sizes.items():
assert row["rendered"], f"{route} {size} 빈 화면"
assert row["pointer"] == ("fine" if size == DESKTOP else "coarse"), (route, size)
# 로그인 뒤 쪽 = ☰ 왼쪽 위 · 그 상자와 겹치는 글 · 단추 0 · 본문 위 끝 0(7-3)
if route[0] in "bm":
assert not row["burger_hits"], (route, size, row["burger_hits"])
assert row["outlet_top"] == (40 if route == "b03-file-input" else 0), (route, size)
if (ROOT / "tmp" / "desktop_before2").exists():
# 차이 0 = 같은 원래 코드 두 번 재기(before · before2) 잡음 이하
noise = {r: layout_diff(r, "before", "before2") for r in PAGES}
over = [r for r in PAGES if layout_diff(r) > noise[r]]
# 공개 쪽 = 배치 그대로 · 로그인 뒤 쪽 = 가로 자리 그대로 — 같은 원래 코드 두 번 재기(before · before2) 잡음 이하
def diff(r, b):
return len(bm_moved(r, "before", b)) if r[0] in "bm" else layout_diff(r, "before", b)
noise = {r: diff(r, "before2") for r in PAGES}
over = [r for r in PAGES if diff(r, "after") > noise[r]]
if over: # 불러오는 때에 따라 한 번 흔들림(B06 종단) — 그 쪽만 한 번 더 잼
run_audit("after_retry", pages=over, sizes={DESKTOP: SIZES[DESKTOP]})
over = {
r: layout_diff(r, "before", "after_retry")
for r in over
if layout_diff(r, "before", "after_retry") > noise[r]
}
over = {r: diff(r, "after_retry") for r in over if diff(r, "after_retry") > noise[r]}
assert not over, over
+51 -17
View File
@@ -1,10 +1,11 @@
"""☰ 서랍 · 위 줄 걷기(PLAN_기기대응 6 · `app_shell.ts` · `ui_template_workflow_layout.ts`) — 실제 브라우저.
"""☰ 서랍 · 위 줄 걷기(PLAN_기기대응 6 · 7 · `app_shell.ts` · `ui_template_workflow_layout.ts`) — 실제 브라우저.
① 휴대폰 · 로그인 뒤 B 쪽 = 위 줄 걷힘(본문 위 끝 0) · ☰ 는 위 오른쪽 모서리 · 떠 있는 진행 판 없음
② ☰ 서랍 = 로고 · 홈 ─ 진행 단계 ─ 메뉴 ─ 언어 · 테마 · 로그인 · 터치 모드 차례 · 진행 단계는 진행 판의 **그 요소**(한 벌)
지금 단계 강조 그대로 · 닫으면 제자리(진행 판)로 돌아감 · 단계 누름 = 이동 + 서랍 닫힘
③ 휴대폰 · 공개 쪽(A01) = 위 줄 그대로(본문 위 끝 64)
④ 데스크탑(1440 · 마우스) = 위 줄 · 진행 판 그대로 · ☰ 없음
① 휴대폰 · 로그인 뒤 B 쪽 = 위 줄 걷힘(본문 위 끝 0) · ☰ 는 위 왼쪽 모서리(폭 36 · 손가락 높이 44) · 떠 있는 진행 판 없음
② ☰ 서랍 = 왼쪽에서 열림 · ☰ 는 서랍 위에 그대로 · 로고 · 홈 ─ 진행 단계 ─ 메뉴 ─ 언어 · 테마 · 로그인 · 터치 모드 차례
진행 단계는 진행 판의 **그 요소**(한 벌) · 지금 단계 강조 그대로 · 닫으면 제자리(진행 판)로 돌아감 · 단계 누름 = 이동 + 서랍 닫힘
③ 휴대폰 · 공개 쪽(A01) = 위 줄 그대로(본문 위 끝 64) · ☰ 는 위 줄 오른쪽
④ 데스크탑(1440 · 마우스) · 로그인 뒤 B 쪽 = 위 줄 걷힘 · ☰ 왼쪽 위 36×36 · 진행 판 없음 · 위 줄 항목이 모두 서랍에 · Esc 로 닫힘
⑤ 데스크탑 · 공개 쪽(A01) = 위 줄 그대로 · ☰ 없음
"""
import json
@@ -18,6 +19,17 @@ 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)"
BURGER = (
"(() => { const r = document.querySelector('.app-burger').getBoundingClientRect();"
" return [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height),"
" Math.round(r.right)]; })()"
)
PROGRESS_HIDDEN = (
"!document.querySelector('.ui-workflow-overlay__panel--progress').getClientRects().length"
)
LABELS = (
"(sel) => [...document.querySelectorAll(sel)].map(b => b.textContent.trim()).filter(Boolean)"
)
def _ctx(browser, cred, size):
@@ -43,20 +55,24 @@ def test_drawer_holds_steps_and_header_folds():
# ①
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"
)
burger = page.evaluate(BURGER)
assert burger[:4] == [8, 8, 36, 44], burger
assert page.evaluate(PROGRESS_HIDDEN)
# ②
page.evaluate(
"window.__steps = document.querySelector('.ui-workflow-overlay__panel--progress .ui-workflow-layout__steps')"
)
page.evaluate("document.querySelector('.app-burger').click()")
assert (
page.evaluate(
"Math.round(document.querySelector('.app-drawer').getBoundingClientRect().left)"
)
== 0
)
assert page.evaluate( # ☰ 는 서랍 위에서 그대로 눌림(다시 누르면 닫힘)
"document.elementFromPoint(26, 30) === document.querySelector('.app-burger')"
)
groups = page.evaluate(GROUPS)
order = [
next((k for k in ("brand", "steps", "nav", "touch") if k in c), "settings")
@@ -87,13 +103,31 @@ def test_drawer_holds_steps_and_header_folds():
# ③
audit._open(page, "a01-home")
assert page.evaluate(OUTLET_TOP) == 64
assert page.evaluate(BURGER)[4] >= 390 - 16
# ④
desk = _ctx(browser, cred, audit.DESKTOP).new_page()
audit._open(desk, "b07-design-detail")
assert desk.evaluate(OUTLET_TOP) == 0
assert desk.evaluate(BURGER)[:4] == [8, 8, 36, 36]
assert desk.evaluate(PROGRESS_HIDDEN)
head_nav = desk.evaluate(LABELS, ".app-nav button")
head_actions = desk.evaluate(LABELS, ".app-actions button")
assert head_nav and head_actions
desk.click(".app-burger")
assert desk.evaluate(LABELS, ".app-drawer__nav button") == head_nav
drawer_all = desk.evaluate(LABELS, ".app-drawer button")
assert all(label in drawer_all for label in head_actions), (head_actions, drawer_all)
assert desk.evaluate(
"document.querySelector('.app-drawer__steps .ui-workflow-layout__step.is-active') !== null"
)
desk.keyboard.press("Escape")
assert not desk.evaluate(
"document.querySelector('.app-header').classList.contains('is-drawer-open')"
)
# ⑤
audit._open(desk, "a01-home")
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()
@@ -111,3 +111,9 @@
padding: var(--spacing-24) var(--spacing-16) var(--spacing-40);
}
}
/* ☰ 들여쓰기(7-2) — 제목 줄이 왼쪽 위 ☰ 자리에 걸칠 때만 비킴(넓은 화면은 가운데 정렬로 이미 비켜 0).
(100vw - 100%) / 2 = 제목 줄 왼쪽 끝 · 세로 스크롤 막대 몫 8 을 더 비움 */
html.is-bm-route .ui-general-layout__header {
padding-left: max(0px, calc(var(--app-burger-inset) + var(--spacing-8) - (100vw - 100%) / 2));
}
+13 -2
View File
@@ -541,8 +541,8 @@ html:is([data-size="compact"], [data-size="medium"])
pointer-events: auto;
}
/* ☰ 가 메뉴를 맡는 때(폭 900 이하 · 터치)는 떠 있는 진행 판 대신 ☰ 서랍 안 진행 단계 묶음(6-2) */
html[data-burger="on"] .ui-workflow-overlay__panel--progress {
/* 로그인 뒤 쪽(B · M)은 떠 있는 진행 판 대신 ☰ 서랍 안 진행 단계 묶음(6-2 · 7-1) */
html.is-bm-route .ui-workflow-overlay__panel--progress {
display: none;
}
@@ -552,3 +552,14 @@ html:is([data-size="compact"], [data-size="medium"])
> :not(.ui-workflow-overlay__handle) {
visibility: hidden;
}
/* ☰ 들여쓰기(7-2) — 왼쪽 위에 뜬 ☰ 자리만큼 좌측 패널 제목 줄을 비킴 · 손가락 ☰(높이 44)는 제목 줄도 그만큼 높임 */
html.is-bm-route .ui-workflow-overlay__panel--title > .ui-workflow-overlay__header {
padding-left: max(var(--spacing-16), var(--app-burger-inset));
}
html.is-bm-route[data-touch="on"]
.ui-workflow-overlay__panel--title
> .ui-workflow-overlay__header {
min-height: calc(var(--spacing-8) * 2 + 44px);
}
@@ -136,3 +136,20 @@
overflow: auto;
background: var(--color-canvas);
}
/* ☰ 들여쓰기(7-2) — 본문 첫 줄이 ☰ 자리를 비키려면 본문 왼쪽 끝에서 더 비울 폭.
좌측 패널이 열려 본문을 민 때는 0 · 접힘(손잡이 24) · 좁은 폭에서 본문 위에 겹친 때는 남은 만큼.
쪽마다 첫 줄(도구 줄 · 탭 · 제목)이 `padding-left: max(제 여백, var(--wf-burger-pad))` 한 줄로 씀 */
html.is-bm-route .ui-workflow-layout {
--wf-burger-pad: max(0px, calc(var(--app-burger-inset) - var(--spacing-24)));
}
html.is-bm-route .ui-workflow-layout.is-options-open {
--wf-burger-pad: 0px;
}
@media (max-width: 860px) {
html.is-bm-route .ui-workflow-layout.is-options-open {
--wf-burger-pad: var(--app-burger-inset);
}
}