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:
+29
-11
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 하면 그리기 전에 적힘.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user