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:
@@ -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,
|
||||
),
|
||||
);
|
||||
// 구조물 우클릭 메뉴 — 측점선 위면 삭제, 빈 자리면 배관 추가. 이동도 여기(측점선 끌기)서 한다.
|
||||
|
||||
@@ -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);
|
||||
// 접힘 상태는 세션에만 남긴다(리사이저와 같은 규칙).
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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°에서 오른쪽(▶)을 가리킨다. 펼친 상태면 접히는 쪽을,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* 위아래로 끌어 높이를 조절 — 패널 위쪽 경계에 가로로 눕는다. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user