fix(B05·B06): 좌측 패널 아래 단추 묶음 바닥 고정 복구(PLAN 44-3)
- 까닭 = 473d959d6 이 좌측 자리를 빈 채 틀에 넘겨 공용 쪼개기 건너뜀 · 보기 전환이 패널 클래스를 통째 덮어 ui-sidebar-fill 지움
- 공용 쪼개기 내보냄 · 두 번 불러도 한 번만 · 패널을 붙인 뒤 다시 부름
- 늦게 서는 칸은 스크롤 칸 안으로 · 보기 클래스는 toggle
- 시험 test_design_panel_dock_fixed · 화면 시나리오 ui_checks/44-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
@@ -112,6 +112,13 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De
|
||||
const dockParts: HTMLElement[] = [];
|
||||
const dock = createDesignDock(view, dockParts, ctx.dock);
|
||||
|
||||
// 칸은 공용 스크롤 칸(쪼갠 뒤) 안에 둔다 — 쪼개기 전이면 패널 루트의 단추 묶음 앞.
|
||||
const place = (element: HTMLElement): void => {
|
||||
const scroll = root.querySelector<HTMLElement>(":scope > .ui-sidebar-scroll");
|
||||
if (scroll) scroll.append(element);
|
||||
else root.insertBefore(element, dock.root);
|
||||
};
|
||||
|
||||
const show = (target: DesignView): void => {
|
||||
const wanted = new Set(panelSectionsFor(target).map((def) => def.id));
|
||||
for (const def of panelSectionsFor(target)) {
|
||||
@@ -120,12 +127,12 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De
|
||||
const sectionRoot = "root" in made ? made.root : made;
|
||||
if ("root" in made && made.dock) dock.root.prepend(made.dock);
|
||||
built.set(def.id, sectionRoot);
|
||||
root.insertBefore(sectionRoot, dock.root);
|
||||
place(sectionRoot);
|
||||
}
|
||||
// 차례를 정의 순서로 다시 맞춘다 — 늦게 만들어진 칸이 맨 아래에 남지 않게.
|
||||
for (const def of panelSectionsFor(target)) {
|
||||
const element = built.get(def.id);
|
||||
if (element) root.insertBefore(element, dock.root);
|
||||
if (element) place(element);
|
||||
}
|
||||
built.forEach((element, id) => {
|
||||
// 칸이 스스로 숨긴 것(선택 포인트)은 건드리지 않고, 보기 밖 칸만 가리고 되살린다.
|
||||
@@ -144,7 +151,9 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De
|
||||
dock,
|
||||
setView: (next) => {
|
||||
dock.setView(next);
|
||||
root.className = next === "profile" ? "b05-route__panel" : "b06-profile__form";
|
||||
// 클래스 통째 덮기 금지 — 공용 쪼개기가 붙인 ui-sidebar-fill 이 지워져 단추 묶음이 바닥에서 뜬다.
|
||||
root.classList.toggle("b05-route__panel", next === "profile");
|
||||
root.classList.toggle("b06-profile__form", next !== "profile");
|
||||
show(next);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { splitSidebarActions } from "@ui/ui_template_overlay";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../b_workflow_nav";
|
||||
import { navigateTo } from "../router";
|
||||
@@ -229,6 +230,9 @@ export function sharedPanel(shared: DesignPageShared): DesignPanel {
|
||||
shared.view(),
|
||||
);
|
||||
shared.left.append(panel.root);
|
||||
// 틀이 먼저 섰으면 좌측 자리가 빈 채로 지나갔다 — 붙인 지금 [스크롤 칸][아래 고정 단추]로 쪼갠다.
|
||||
const body = shared.left.closest<HTMLElement>(".ui-workflow-overlay__body");
|
||||
if (body) splitSidebarActions(body);
|
||||
shared.panel = panel;
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""B05 · B06 좌측 패널 아래 고정(계획서 44-3) — 단추 묶음이 패널 바닥에 붙고 위 칸만 스크롤.
|
||||
|
||||
공용 쪼개기(`splitSidebarActions`)가 [스크롤 칸][아래 고정 단추] 로 나누는데, 한 페이지(design_page)는
|
||||
좌측 자리를 빈 채 틀에 넘겨 쪼개기가 지나가고, 보기 전환이 패널 클래스를 통째 덮어 `ui-sidebar-fill` 을
|
||||
지웠다. 세 자리가 다시 어긋나면 빨강. 화면 수치는 `ui_checks/44-3_좌측패널_하단고정.js`.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OVERLAY = ROOT / "ui_template" / "ui_template_overlay.ts"
|
||||
PANEL = ROOT / "A00_Common" / "design" / "design_panel.ts"
|
||||
SHARED = ROOT / "A00_Common" / "design" / "design_shared.ts"
|
||||
|
||||
|
||||
def read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_split_is_exported_and_idempotent():
|
||||
src = read(OVERLAY)
|
||||
assert "export function splitSidebarActions(" in src
|
||||
assert 'if (body.classList.contains("ui-sidebar-split")) return;' in src
|
||||
|
||||
|
||||
def test_shared_panel_splits_after_attach():
|
||||
src = read(SHARED)
|
||||
attach = src.index("shared.left.append(panel.root);")
|
||||
assert "splitSidebarActions(body)" in src[attach : attach + 400]
|
||||
|
||||
|
||||
def test_panel_keeps_fill_class_and_scroll_host():
|
||||
src = read(PANEL)
|
||||
# 보기 전환이 클래스를 통째 덮으면 ui-sidebar-fill 이 지워져 단추 묶음이 바닥에서 뜬다.
|
||||
assert "root.className = next" not in src
|
||||
# 늦게 서는 칸은 스크롤 칸 안으로 — 패널 루트에 바로 끼우면 스크롤 밖에 남는다.
|
||||
assert ":scope > .ui-sidebar-scroll" in src
|
||||
@@ -0,0 +1,69 @@
|
||||
/* 44-3 — B05 · B06 좌측 패널 아래 고정(구조물 목록 · 초기화 · 저장 · 보기 전환 단추 묶음)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 좌측 패널 열림 · 프로젝트 = 환경 배정 storage 프로젝트
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 기대 수치(B05 → B06 → B05 · 각 보기에서 보통 · 긴 패널 둘):
|
||||
* - split = true · 패널 루트 자식 = [ui-sidebar-scroll, 단추 묶음] · 칸은 모두 스크롤 칸 안(loose = 0)
|
||||
* - dockGap = 패널 몸통 바닥 − 단추 묶음 바닥 = 0(±1px)
|
||||
* - 긴 패널(스크롤 칸에 2000px 임시 칸): overflow = true · dockGap 그대로 0 · 단추 묶음 top 안 바뀜
|
||||
* - 끝나면 임시 칸 지우고 처음 주소로 되돌림
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const start = location.hash;
|
||||
const measure = async (tag) => {
|
||||
const panel = document.querySelector(".b05-route__panel, .b06-profile__form");
|
||||
if (!panel) return { tag, error: "패널 없음" };
|
||||
const body = panel.closest(".ui-workflow-overlay__body");
|
||||
const scroll = panel.querySelector(":scope > .ui-sidebar-scroll");
|
||||
const dock = panel.querySelector(":scope > .ui-sidebar-actions");
|
||||
const kids = [...panel.children].map((c) => c.classList[0]);
|
||||
const gap = () =>
|
||||
Math.round(body.getBoundingClientRect().bottom - dock.getBoundingClientRect().bottom);
|
||||
const res = {
|
||||
tag,
|
||||
view: panel.classList.contains("b06-profile__form") ? "B06" : "B05",
|
||||
split: body.classList.contains("ui-sidebar-split"),
|
||||
kids,
|
||||
loose: kids.filter((k) => k !== "ui-sidebar-scroll" && !dock?.classList.contains(k)).length,
|
||||
dockGap: dock ? gap() : null,
|
||||
};
|
||||
if (!scroll || !dock) return res;
|
||||
const top0 = Math.round(dock.getBoundingClientRect().top);
|
||||
const tall = document.createElement("div");
|
||||
tall.style.cssText = "height: 2000px; flex: none";
|
||||
scroll.append(tall);
|
||||
await w(100);
|
||||
res.long = {
|
||||
overflow: scroll.scrollHeight > scroll.clientHeight,
|
||||
dockGap: gap(),
|
||||
dockMoved: Math.round(dock.getBoundingClientRect().top) - top0,
|
||||
};
|
||||
tall.remove();
|
||||
res.ok =
|
||||
res.split &&
|
||||
res.loose === 0 &&
|
||||
Math.abs(res.dockGap) <= 1 &&
|
||||
res.long.overflow &&
|
||||
Math.abs(res.long.dockGap) <= 1 &&
|
||||
res.long.dockMoved === 0;
|
||||
return res;
|
||||
};
|
||||
const go = async (hash) => {
|
||||
location.hash = hash;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await w(250);
|
||||
const want = hash.includes("b06") ? ".b06-profile__form" : ".b05-route__panel";
|
||||
if (document.querySelector(want + " > .ui-sidebar-actions")) break;
|
||||
}
|
||||
await w(500);
|
||||
};
|
||||
const out = [];
|
||||
await go("#/b05-profile");
|
||||
out.push(await measure("B05"));
|
||||
await go("#/b06-section");
|
||||
out.push(await measure("B06"));
|
||||
await go("#/b05-profile");
|
||||
out.push(await measure("B05 다시"));
|
||||
if (location.hash !== start) await go(start);
|
||||
return JSON.stringify({ ok: out.every((r) => r.ok), out });
|
||||
})();
|
||||
@@ -112,7 +112,9 @@ function readOpenState(key: string, collapseWhenNarrow = false): boolean {
|
||||
* 아예 빼고, 나머지 그룹만 마진·패딩 없는 래퍼(`.ui-sidebar-scroll`)에 담아 그쪽만
|
||||
* 스크롤시킨다. 페이지는 지금처럼 본문 마지막에 액션 줄을 붙이기만 하면 된다.
|
||||
*/
|
||||
function splitSidebarActions(body: HTMLElement): void {
|
||||
export function splitSidebarActions(body: HTMLElement): void {
|
||||
// 이미 쪼갠 body 는 그대로 — 패널을 늦게 붙이는 화면(B05·B06 한 페이지)이 붙인 뒤 다시 부른다.
|
||||
if (body.classList.contains("ui-sidebar-split")) return;
|
||||
// 액션 줄은 보통 페이지 패널 루트(b05-route__panel 등) **안**에 있다 — body 직계가
|
||||
// 아니므로 깊이 무관하게 찾고, 액션 줄의 실제 부모 기준으로 쪼갠다.
|
||||
const actions = body.querySelector<HTMLElement>(".ui-sidebar-actions");
|
||||
|
||||
Reference in New Issue
Block a user