feat(B05/B06/ui): 패널 손잡이·리사이저·편집 버튼 시인성 정리(16·18~20)

- 16: 편집 버튼 전체(▲▼·↺·⬆⬇) 21×17 통일, 겹침 간격 23px, X축 라벨 상향 19px
- 18: 유토곡선 손잡이가 오버레이 위 경계를 따라 이동(펼침 시 테이블 영역 침범 허용)
- 19: 손잡이 테두리 강조색 --panel-handle-border(라이트 파랑 #2563eb / 다크 노랑
  #e0b64a) + 이름표 옵션 — B05 '종단'·'유토곡선'·세로 '배수유역', B06 '종단/유토곡선'
- 20: 공용 리사이저 상시 표시선(1px, 같은 강조색) — hover/드래그 시 3px

Playwright: 버튼 3종 21×17, 라벨·테두리색·세로 writing-mode, 리사이저 1px/0.75,
손잡이 bottom=오버레이 top 일치, 콘솔 오류 0. typecheck 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 19:06:02 +09:00
co-authored by Claude Opus 5
parent cdd40a00f3
commit d4fc73c012
10 changed files with 144 additions and 27 deletions
@@ -103,7 +103,8 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
root.className = "b05-drainage";
// 이 패널은 하단 패널의 **오른쪽**에 도킹해 오른쪽으로 접힌다(제목 카드와 반대).
// 방향을 넘기지 않으면 화살표가 정확히 거꾸로 나온다.
const panelHandle = createWorkflowPanelHandle("side", "right");
// 이름표 "배수유역" — 세로(90°)로 화살표 위에 얹힌다(2026-08-04 사용자 지시).
const panelHandle = createWorkflowPanelHandle("side", "right", "배수유역");
const header = document.createElement("div");
header.className = "b05-drainage__header";
@@ -23,9 +23,9 @@ import type {
import { chainageKey, emptyEdits, hasEdits } from "./B05_wf2_Route_UI_Profile_Alignment";
const DRAFT_KEY_PREFIX = "b05-profile-alignment-draft";
/** 버튼 폭(18px)에 여유를 더한 값 — 이보다 가까우면 겹친 것으로 본다. */
const BUTTON_CLEARANCE_PX = 20;
const BUTTON_HALF_PX = 9;
/** 버튼 폭(21px)에 여유를 더한 값 — 이보다 가까우면 겹친 것으로 본다. */
const BUTTON_CLEARANCE_PX = 23;
const BUTTON_HALF_PX = 10;
/** 길게 누르기: 이만큼 유지하면 반복이 시작되고, 그 뒤 초당 10회(0.1m씩)로 이어진다. */
const HOLD_DELAY_MS = 500;
const HOLD_INTERVAL_MS = 100;
@@ -154,7 +154,7 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
// 손잡이는 다른 패널과 **같은 양식**의 표준 삼각형 손잡이 하나만 쓴다(2026-08-04 사용자
// 지시 — 예전 풀폭 바 + "유토곡선" 캡션은 다른 패널들과 모양이 달랐다). 무엇의 손잡이인지는
// 툴팁으로 밝힌다.
const handleControl = createWorkflowPanelHandle("bottom", "down");
const handleControl = createWorkflowPanelHandle("bottom", "down", "유토곡선");
const handle = document.createElement("div");
handle.className = "b05-profile__masshaul-handle";
handle.append(handleControl.root);
@@ -180,7 +180,8 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
max: () => (overlay.parentElement?.clientHeight ?? window.innerHeight) * OVERLAY_MAX_RATIO,
storageKey: OVERLAY_HEIGHT_KEY,
onResize: () => {
// 끄는 동안 매 이동마다 전체 redraw를 돌리면 손을 못 따라간다 — 프레임당 한 번만.
// 손잡이는 경계를 즉시 따라가고(가벼움), 전체 redraw는 프레임당 한 번만.
syncHandlePosition();
if (resizeRedrawPending) return;
resizeRedrawPending = true;
requestAnimationFrame(() => {
@@ -211,6 +212,15 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
let open = sessionStorage.getItem(OPEN_KEY) === "true";
let context: RouteMassHaulContext | null = null;
/**
* 손잡이를 오버레이 **위 경계**에 태운다(2026-08-04 사용자 지시 — 펼치면 같이 올라와
* 테이블 영역을 침범해도 된다). 접혀 있으면 패널 바닥으로 돌아온다. 높이 조절 중에도
* 리사이저 onResize → redraw 경로에서 다시 불려 항상 경계를 따라다닌다.
*/
function syncHandlePosition(): void {
handle.style.bottom = open ? `${overlay.offsetHeight}px` : "0px";
}
function applyOpen(next: boolean): void {
open = next;
sessionStorage.setItem(OPEN_KEY, String(next));
@@ -219,6 +229,7 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
handleControl.root.title = next ? "유토곡선 접기" : "유토곡선 펼치기";
handle.classList.toggle("is-open", next);
overlay.hidden = !next;
syncHandlePosition();
onChanged();
}
handleControl.setOpen(open);
@@ -264,6 +275,7 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
function draw(params: RouteMassHaulDrawParams): void {
overlay.hidden = !open;
syncHandlePosition();
if (!open) return;
if (!context) {
note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다.");
@@ -225,7 +225,8 @@ export function createRouteProfilePanel(
) {
const root = document.createElement("section");
root.className = "b05-route-profile";
const panelHandle = createWorkflowPanelHandle("bottom");
// 이름표 "종단" — 무엇의 손잡이인지 화살표 옆에 밝힌다(2026-08-04 사용자 지시).
const panelHandle = createWorkflowPanelHandle("bottom", "down", "종단");
const balanceBar = document.createElement("div");
balanceBar.className = "b05-route-profile__balance";
const body = document.createElement("div");
@@ -539,9 +540,9 @@ export function createRouteProfilePanel(
);
if (target) callbacks?.onStructureMove?.(target.chainage_m, toChainage, target);
},
// 계획고 편집 ▼ 버튼(바닥 2px + 높이 15px)과 측점 라벨이 겹치지 않게 X축·라벨을
// 계획고 편집 ▼ 버튼(바닥 2px + 높이 17px)과 측점 라벨이 겹치지 않게 X축·라벨을
// 버튼 높이만큼 올린다(2026-08-04 사용자 지시). B06은 편집 버튼이 없어 0 유지.
17,
19,
),
);
// 구조물 우클릭 메뉴 — 측점선 위면 삭제, 빈 자리면 배관 추가. 이동도 여기(측점선 끌기)서 한다.
+8 -9
View File
@@ -835,12 +835,15 @@
pointer-events: none;
}
/* 상시 보이게 둔다(예전엔 패널 hover 시에만 노출). 크기(18×15)는 유지하고 글자는 밝게. */
/* 상시 보이게 둔다(예전엔 패널 hover 시에만 노출). 크기는 ▲▼·↺·⬆⬇ 전부 21×17로
키웠다(2026-08-04 사용자 지시 — 18×15는 누르기도 읽기도 작았다). */
.b05-profile-edit__btn {
position: absolute;
width: 18px;
height: 15px;
width: 21px;
height: 17px;
padding: 0;
font-size: 12px;
line-height: 1;
border: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent);
border-radius: 3px;
background: color-mix(in srgb, var(--color-surface-raised) 80%, transparent);
@@ -883,13 +886,9 @@
color: var(--color-danger);
}
/* 구간 이동(⬆⬇)은 측점 버튼보다 살짝 크게 — 속 찬 화살표가 잘 읽히도록(2026-08-04). */
/* 구간 이동(⬆⬇)은 속 찬 화살표가 잘 읽히도록 굵게(크기는 공통 21×17). */
.b05-profile-edit__btn.is-segment {
width: 21px;
height: 17px;
font-size: 12px;
font-weight: 700;
line-height: 1;
}
.b05-profile-edit__btn.is-down {
@@ -912,7 +911,7 @@
}
.b05-profile-edit__btn.is-reset {
top: 20px;
top: 22px;
border-color: color-mix(in srgb, var(--color-royal-amethyst, rgb(109 40 217)) 55%, transparent);
background: var(--color-surface-raised);
color: color-mix(in srgb, var(--color-royal-amethyst, rgb(139 92 246)) 85%, var(--color-text));
@@ -223,7 +223,7 @@ export function createSectionView(
// 공용 `ui-collapsible__title`은 **쓰지 않는다**. 그 클래스를 달면 전역 규칙이 캐럿(▾/▸)을
// 하나 더 그리는데, 접힘 상태 전역 선택자(`.ui-collapsible.is-collapsed > .ui-collapsible__title`)가
// B06 쪽 무력화 규칙보다 특정도가 높아 캐럿이 삼각형 아이콘과 겹쳐 보였다. 토글은 여기서 직접 한다.
const panelToggle = createWorkflowPanelHandle("bottom", "up");
const panelToggle = createWorkflowPanelHandle("bottom", "up", "종단/유토곡선");
panelToggle.root.classList.add("b06-section__panel-toggle");
panel.append(panelHeader, panelBody, panelToggle.root);
// 접힘 상태는 세션에만 남긴다(리사이저와 같은 규칙).
+46 -1
View File
@@ -84,7 +84,9 @@
.ui-workflow-overlay__handle {
position: absolute;
border: 1px solid var(--color-border);
/* 배경과 같은 무채색 테두리는 손잡이가 묻혀 안 보였다(2026-08-04 사용자 지시).
테마별 강조색 테두리 — 라이트: 파랑, 다크: 노랑(--panel-handle-border, 테마 정의). */
border: 1px solid var(--panel-handle-border, var(--color-border));
background: var(--color-surface-raised);
color: var(--color-text);
box-shadow: var(--shadow-lg);
@@ -103,6 +105,49 @@
transition: transform var(--transition-fast);
}
/* 이름표 달린 손잡이 — 화살표+텍스트를 나란히 담도록 고정 폭을 푼다. */
.ui-workflow-overlay__handle.is-labeled {
display: flex;
gap: var(--spacing-4);
align-items: center;
justify-content: center;
}
.ui-workflow-overlay__handle-label {
color: var(--color-text-secondary);
font-size: var(--text-caption);
line-height: 1;
white-space: nowrap;
user-select: none;
}
/* bottom 손잡이: 화살표 오른쪽에 가로 이름표 — 폭은 내용에 맞춘다(최소 64px 유지). */
.ui-workflow-overlay__handle--bottom.is-labeled {
width: auto;
min-width: var(--spacing-64);
padding: 0 var(--spacing-8);
}
.ui-workflow-overlay__handle--bottom.is-labeled .ui-workflow-overlay__handle-icon {
margin: 0;
}
/* side 손잡이: 화살표 위쪽에 90° 세로 이름표 — 높이는 내용에 맞춘다(최소 64px 유지). */
.ui-workflow-overlay__handle--side.is-labeled {
flex-direction: column;
height: auto;
min-height: var(--spacing-64);
padding: var(--spacing-8) 0;
}
.ui-workflow-overlay__handle--side.is-labeled .ui-workflow-overlay__handle-icon {
margin: 0;
}
.ui-workflow-overlay__handle--side.is-labeled .ui-workflow-overlay__handle-label {
writing-mode: vertical-rl;
}
/* title 패널 우측 가장자리 세로 중앙 핸들 — 닫히면 이 버튼만 화면 왼쪽에 남음 */
.ui-workflow-overlay__handle--side {
top: 50%;
+17 -1
View File
@@ -38,13 +38,29 @@ export function createWorkflowPanelHandle(
* 넘겨야 한다. 좌우와 같은 이유로, 도킹 방향이 반대인데 같은 회전을 쓰면 표시가 거꾸로 된다.
*/
collapseToward: "left" | "right" | "up" | "down" = "left",
/**
* 손잡이 옆에 붙일 짧은 이름표(선택, 2026-08-04 사용자 지시 — 무엇의 손잡이인지 보이게).
* bottom 손잡이는 화살표 **오른쪽**에 가로로, side 손잡이는 화살표 **위쪽**에 90° 세로로
* 놓인다. 사이드바(제목 패널)처럼 이름이 필요 없는 곳은 생략하면 현행과 같다.
*/
label?: string,
): WorkflowPanelHandle {
const root = document.createElement("button");
root.type = "button";
root.className = `ui-workflow-overlay__handle ui-workflow-overlay__handle--${placement}`;
const icon = document.createElement("span");
icon.className = "ui-workflow-overlay__handle-icon";
root.append(icon);
if (label) {
root.classList.add("is-labeled");
const text = document.createElement("span");
text.className = "ui-workflow-overlay__handle-label";
text.textContent = label;
// side는 이름표가 화살표 위, bottom은 화살표 오른쪽 — DOM 순서로 자리를 정한다.
if (placement === "side") root.append(text, icon);
else root.append(icon, text);
} else {
root.append(icon);
}
function setOpen(isOpen: boolean): void {
// 삼각형은 회전 0°에서 오른쪽(▶)을 가리킨다. 펼친 상태면 접히는 쪽을,
+45 -6
View File
@@ -1,18 +1,57 @@
/* 공통 스플리터(드래그 리사이저) — 패널 경계에 얹는 얇은 손잡이.
레이아웃을 밀지 않도록 경계 위에 절대배치하고, 평소에는 보이지 않다가
마우스를 얹거나 끌 때만 색이 들어온다. */
레이아웃을 밀지 않도록 경계 위에 절대배치한다.
조절 가능한 패널이 여러 곳(B05 3곳·B06 1곳)인데 hover 전에는 보이지 않아 구분이
안 됐다(2026-08-04 사용자 지시) — 손잡이 테두리와 같은 강조색(--panel-handle-border)
의 **얇은 줄을 상시 표시**하고, 얹거나 끌 때 굵어진다. 히트 영역(6px)은 그대로다. */
.ui-resizer {
position: absolute;
z-index: 1;
background: transparent;
transition: background var(--transition-fast);
touch-action: none;
}
.ui-resizer:hover,
.ui-resizer.is-dragging {
background: var(--color-primary);
/* 상시 보이는 얇은 표시줄 — 히트 영역 가운데 1px. */
.ui-resizer::before {
content: "";
position: absolute;
background: var(--panel-handle-border, var(--color-primary));
opacity: 0.75;
transition:
opacity var(--transition-fast),
background var(--transition-fast);
}
.ui-resizer--vertical::before {
top: 50%;
right: 0;
left: 0;
height: 1px;
transform: translateY(-50%);
}
.ui-resizer--horizontal::before {
top: 0;
bottom: 0;
left: 50%;
width: 1px;
transform: translateX(-50%);
}
.ui-resizer:hover::before,
.ui-resizer.is-dragging::before {
opacity: 1;
}
.ui-resizer--vertical:hover::before,
.ui-resizer--vertical.is-dragging::before {
height: 3px;
}
.ui-resizer--horizontal:hover::before,
.ui-resizer--horizontal.is-dragging::before {
width: 3px;
}
/* 위아래로 끌어 높이를 조절 — 패널 위쪽 경계에 가로로 눕는다. */
+4
View File
@@ -35,6 +35,8 @@
--color-success: #1f8a4c; /* 성공/승인 */
--color-warning: #b8860b; /* 경고 (design.md 별색 #f5b800 계열) */
--color-danger: #c0392b; /* 에러/삭제 */
/* 패널 접기/펼치기 손잡이 테두리 — 배경에 묻히지 않는 강조색(라이트: 파랑, 다크: 노랑) */
--panel-handle-border: #2563eb;
--color-info: var(--color-royal-amethyst);
/* 그라디언트 (Gradient System) — 항상 흰 표면 뒤에만 배치, 텍스트 직상단 금지 */
@@ -300,6 +302,7 @@
--color-success: #3fbb6f;
--color-warning: #e0b64a;
--color-danger: #e8695a;
--panel-handle-border: #e0b64a;
--shadow-sm: rgba(0, 0, 0, 0.4) 0px 2px 4px 0px, rgba(0, 0, 0, 0.2) 0px 0px 0px 1px;
--shadow-lg: rgba(0, 0, 0, 0.5) 0px 32px 24px -12px, rgba(0, 0, 0, 0.3) 0px 3px 3px 0px;
@@ -335,6 +338,7 @@
--color-primary-text: #16121f;
--color-accent: var(--color-lavender-wash);
--color-focus-ring: var(--color-lavender-wash);
--panel-handle-border: #e0b64a;
}
}