From 3a86e02747eaa36c5d63b884c78f1e484bc22100 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 15:31:46 +0900 Subject: [PATCH] =?UTF-8?q?fix(B07):=20=EB=A6=AC=EB=B3=B8=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=93=9C=EC=95=84?= =?UTF-8?q?=EC=9B=83=EC=9D=B4=20=EC=9E=98=EB=A0=A4=20=EC=BA=94=EB=B2=84?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20=EA=B0=80=EB=A6=AC=EB=8D=98=20=EA=B2=83?= =?UTF-8?q?=EC=9D=84=20=EB=9D=84=EC=9A=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 패널 줄이 가로 스크롤 상자(overflow-x: auto)라, CSS 규칙상 세로도 auto로 계산되어 슬라이드아웃 메뉴가 리본 높이에서 잘렸다. 캔버스에 가려 보이는 증상이 이것이다. 메뉴를 화면 좌표(position: fixed)로 띄우고 라벨 버튼의 위치에서 자리를 잡는다. 오른쪽·아래로 넘치지 않게 좌표를 물리고 높이를 72vh로 제한했다. 캔버스 위에 뜨게 됐으므로 바깥을 누르면 닫히고, 다른 패널을 열면 먼저 열린 것이 닫힌다. Co-Authored-By: Claude Opus 5 (1M context) --- B07_DesignDetail/openwebcad/src/App.css | 9 ++-- .../openwebcad/src/ribbon/Ribbon.tsx | 51 ++++++++++++++++--- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/App.css b/B07_DesignDetail/openwebcad/src/App.css index ef48df68..264f6190 100644 --- a/B07_DesignDetail/openwebcad/src/App.css +++ b/B07_DesignDetail/openwebcad/src/App.css @@ -211,14 +211,15 @@ body > canvas[data-id="canvas"] { color: var(--cad-accent); } .cad-ribbon-overflow { - position: absolute; - z-index: 10; - top: 100%; - left: 4px; + /* 리본 패널 줄이 가로 스크롤 상자라 그 안에서는 잘린다 — 화면 좌표로 띄운다 */ + position: fixed; + z-index: 20; display: flex; flex-direction: column; min-width: 170px; + max-height: 72vh; padding: 4px; + overflow-y: auto; background: var(--cad-chrome-raised); border: 1px solid var(--cad-line); border-radius: 4px; diff --git a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx index cbf9ad42..57fe1f00 100644 --- a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx +++ b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx @@ -1,11 +1,11 @@ /** 리본 렌더러 — ribbon.config.ts의 데이터와 명령 레지스트리만 보고 그린다. */ -import { type FC, type ReactNode, useState } from 'react'; -import { getActiveRibbonTab, setActiveRibbonTab } from '../components/ui-state'; +import { type FC, type ReactNode, useEffect, useRef, useState } from 'react'; import type { CadCommand } from '../commands/command.types'; import { getCommandById } from '../commands/registry'; import { runCommand } from '../commands/run-command'; -import { type RibbonPanel, RIBBON_TABS, type RibbonWidget } from './ribbon.config'; +import { getActiveRibbonTab, setActiveRibbonTab } from '../components/ui-state'; import type { Tool } from '../tools'; +import { RIBBON_TABS, type RibbonPanel, type RibbonWidget } from './ribbon.config'; interface RibbonProps { activeTool: Tool | null; @@ -36,6 +36,9 @@ const CommandButton: FC = ({ command, size, activeTool, onRu ); +/** 슬라이드아웃 최소 폭 — 화면 오른쪽으로 넘치지 않게 자리를 잡을 때 쓴다 */ +const OVERFLOW_WIDTH = 170; + const resolve = (ids: string[] | undefined): CadCommand[] => (ids ?? []).map((id) => getCommandById(id)).filter((command): command is CadCommand => !!command); @@ -45,9 +48,40 @@ const Panel: FC<{ widgets: Partial>; onRun: (command: CadCommand) => void; }> = ({ panel, activeTool, widgets, onRun }) => { - const [overflowOpen, setOverflowOpen] = useState(false); + // 패널 슬라이드아웃은 화면 좌표로 띄운다 — 리본 패널 줄이 가로 스크롤 상자라 + // 그 안에 두면 리본 높이에서 잘려 캔버스에 가린 것처럼 보인다. + const labelRef = useRef(null); + const [overflowAt, setOverflowAt] = useState<{ top: number; left: number } | null>(null); const overflowCommands = resolve(panel.overflow); + // 캔버스 위에 뜨는 메뉴라 다른 곳을 누르면 닫는다 + useEffect(() => { + if (!overflowAt) return; + const close = (event: PointerEvent) => { + const target = event.target as Node | null; + // 자기 메뉴·자기 라벨만 예외다 — 다른 패널을 누르면 이 메뉴는 닫힌다 + if (target instanceof HTMLElement && target.closest('.cad-ribbon-overflow')) return; + if (target && labelRef.current?.contains(target)) return; + setOverflowAt(null); + }; + document.addEventListener('pointerdown', close); + return () => document.removeEventListener('pointerdown', close); + }, [overflowAt]); + + const toggleOverflow = () => { + if (!overflowCommands.length) return; + if (overflowAt) { + setOverflowAt(null); + return; + } + const rect = labelRef.current?.getBoundingClientRect(); + if (!rect) return; + setOverflowAt({ + top: rect.bottom + 2, + left: Math.max(4, Math.min(rect.left, window.innerWidth - OVERFLOW_WIDTH - 8)), + }); + }; + return (
@@ -79,23 +113,24 @@ const Panel: FC<{ )}
- {overflowOpen && overflowCommands.length > 0 && ( -
+ {overflowAt && overflowCommands.length > 0 && ( +
{overflowCommands.map((command) => (