From d4fc73c0121789eacc6c544087ea0bf96da7e44b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 4 Aug 2026 19:06:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05/B06/ui):=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=86=90=EC=9E=A1=EC=9D=B4=C2=B7=EB=A6=AC=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A0=80=C2=B7=ED=8E=B8=EC=A7=91=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=8B=9C=EC=9D=B8=EC=84=B1=20=EC=A0=95=EB=A6=AC(16=C2=B718~20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../B05_wf2_Route_UI_Drainage_Panel.ts | 3 +- .../B05_wf2_Route_UI_Profile_Edit.ts | 6 +-- .../B05_wf2_Route_UI_Profile_MassHaul.ts | 16 +++++- .../B05_wf2_Route_UI_Profile_Panel.ts | 7 +-- B05_wf2_Route/B05_wf2_Route_UI_Style.css | 17 +++---- .../B06_wf3_ProfileCross_UI_Section_View.ts | 2 +- ui_template/ui_template_overlay.css | 47 ++++++++++++++++- ui_template/ui_template_overlay.ts | 18 ++++++- ui_template/ui_template_resizer.css | 51 ++++++++++++++++--- ui_template/ui_template_theme.css | 4 ++ 10 files changed, 144 insertions(+), 27 deletions(-) diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts index c6692c9e..2dc688cd 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts @@ -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"; diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts index caad1796..180395ba 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts @@ -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; diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts index bf0c81c3..e3e47c9d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts @@ -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("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다."); diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts index dde9bf6e..49a8499a 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -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, ), ); // 구조물 우클릭 메뉴 — 측점선 위면 삭제, 빈 자리면 배관 추가. 이동도 여기(측점선 끌기)서 한다. diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index fd250699..1982d631 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -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)); diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts index 741f892a..fcaa9d4d 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts @@ -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); // 접힘 상태는 세션에만 남긴다(리사이저와 같은 규칙). diff --git a/ui_template/ui_template_overlay.css b/ui_template/ui_template_overlay.css index 8d5c82ef..763dc2b6 100644 --- a/ui_template/ui_template_overlay.css +++ b/ui_template/ui_template_overlay.css @@ -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%; diff --git a/ui_template/ui_template_overlay.ts b/ui_template/ui_template_overlay.ts index 6369ec7b..1faed533 100644 --- a/ui_template/ui_template_overlay.ts +++ b/ui_template/ui_template_overlay.ts @@ -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°에서 오른쪽(▶)을 가리킨다. 펼친 상태면 접히는 쪽을, diff --git a/ui_template/ui_template_resizer.css b/ui_template/ui_template_resizer.css index 90453421..9c9531af 100644 --- a/ui_template/ui_template_resizer.css +++ b/ui_template/ui_template_resizer.css @@ -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; } /* 위아래로 끌어 높이를 조절 — 패널 위쪽 경계에 가로로 눕는다. */ diff --git a/ui_template/ui_template_theme.css b/ui_template/ui_template_theme.css index 2a9562fd..03768d09 100644 --- a/ui_template/ui_template_theme.css +++ b/ui_template/ui_template_theme.css @@ -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; } }