fix(B07): 리본 패널 슬라이드아웃이 잘려 캔버스에 가리던 것을 띄운다

패널 줄이 가로 스크롤 상자(overflow-x: auto)라, CSS 규칙상 세로도 auto로 계산되어
슬라이드아웃 메뉴가 리본 높이에서 잘렸다. 캔버스에 가려 보이는 증상이 이것이다.

메뉴를 화면 좌표(position: fixed)로 띄우고 라벨 버튼의 위치에서 자리를 잡는다.
오른쪽·아래로 넘치지 않게 좌표를 물리고 높이를 72vh로 제한했다.
캔버스 위에 뜨게 됐으므로 바깥을 누르면 닫히고, 다른 패널을 열면 먼저 열린 것이 닫힌다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 15:31:46 +09:00
co-authored by Claude Opus 5
parent ea5ce92725
commit 3a86e02747
2 changed files with 48 additions and 12 deletions
+5 -4
View File
@@ -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;
@@ -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<CommandButtonProps> = ({ command, size, activeTool, onRu
</button>
);
/** 슬라이드아웃 최소 폭 — 화면 오른쪽으로 넘치지 않게 자리를 잡을 때 쓴다 */
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<Record<RibbonWidget, ReactNode>>;
onRun: (command: CadCommand) => void;
}> = ({ panel, activeTool, widgets, onRun }) => {
const [overflowOpen, setOverflowOpen] = useState(false);
// 패널 슬라이드아웃은 화면 좌표로 띄운다 — 리본 패널 줄이 가로 스크롤 상자라
// 그 안에 두면 리본 높이에서 잘려 캔버스에 가린 것처럼 보인다.
const labelRef = useRef<HTMLButtonElement>(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 (
<section className="cad-ribbon-group">
<div className="cad-ribbon-tools">
@@ -79,23 +113,24 @@ const Panel: FC<{
)}
</div>
<button
ref={labelRef}
type="button"
className="cad-ribbon-group__label"
data-expandable={overflowCommands.length > 0}
onClick={() => overflowCommands.length && setOverflowOpen((open) => !open)}
onClick={toggleOverflow}
>
{panel.label}
{overflowCommands.length ? ' ▾' : ''}
</button>
{overflowOpen && overflowCommands.length > 0 && (
<div className="cad-ribbon-overflow">
{overflowAt && overflowCommands.length > 0 && (
<div className="cad-ribbon-overflow" style={{ top: overflowAt.top, left: overflowAt.left }}>
{overflowCommands.map((command) => (
<button
type="button"
key={command.id}
title={command.hint}
onClick={() => {
setOverflowOpen(false);
setOverflowAt(null);
onRun(command);
}}
>